Skip to main content
IntentLang
Draft documentation. Syntax and behavior are illustrative and will change before v1.

Structured Editing and Sync

Not everyone who owns intent writes IntentLang by hand. A product manager thinks in fields: the goal, the guarantees, the inputs, what must never happen. A structured editor (like SkillsTech Studio) lets them edit those fields, but the .intent file must stay the source of truth , and a human's comments must survive the round trip. That is the job of the sync and patch API.

It has three pieces, all deterministic (no AI) and browser-safe via @skillstech/intentlang/core, so a web editor uses them with no Node build:

  • parseToStructured(source) , the canonical structured view a UI renders from.
  • proposeIntent(structured, { base }) , a reviewable proposal from an edited graph.
  • applyEdits(source, edits) , lossless, comment-preserving field edits on the source.

Draft: a brief into rigorous intent

Authoring intent by hand is friction most developers skip. draftIntent(brief) removes it: give a structured brief , the essentials you already know , and it scaffolds a canonical, parseable intent draft and a review checklist of exactly what is still missing. No AI runs; the draft is a proposal, never marked verified. (An agent can produce the brief from a free-text request , via the intent_draft MCP tool , and a human approves the result.)

import { draftIntent } from "@skillstech/intentlang/core";

const { source, review } = draftIntent({
  mission: "create invoice",
  goal: "Create exactly one invoice for an approved order.",
  guarantees: [{ statement: "no duplicate invoice", verify: "idempotency test" }, "totals are never negative"],
  inputs: [{ name: "paymentToken", type: "Secret" }],
});
// source -> canonical IntentLang
// review -> [ "Guarantee 'totals are never negative' has no verification ...",
//             "Input 'paymentToken' is a secret , add a never-expose rule ..." ]

From the shell: intent draft --brief brief.json (or --brief - for stdin), --write out.intent to save. The gaps print so you know what to fill in before the draft becomes a commitment.

Read: source to structured

parseToStructured returns the canonical Intent Graph (what a projector maps to its own nodes and edges) plus a flat, PM-friendly field summary:

import { parseToStructured } from "@skillstech/intentlang/core";

const structured = parseToStructured(source);
// -> { schema: "intent-sync-v1", mission, graph, fields: {
//      goal, why, guarantees[], neverRules[], inputs[], outputs[], decisions[], ...
//    } }

IL stays the source of truth: the structured view is a projection, not a fork.

Propose: structured to source, with a reviewable diff

When the user edits the structured graph, proposeIntent regenerates IntentLang source and, against a base, a reviewable diff , it never applies a silent rewrite:

import { proposeIntent } from "@skillstech/intentlang/core";

const p = proposeIntent(editedStructured, { base: source });
// -> { ok, source, diff, ambiguities, lostNodes, validation, warnings, applied: false }
  • diff , added / removed / changed nodes and relationships, plus any approvals the change invalidates. Render it; let the human apply it.
  • ambiguities , every node whose classification is not factual (proposed, assumed, inferred, unknown). These are proposals to confirm, not guesses to accept.
  • validation , the canonical-vocabulary and no-dangling check on the proposed graph.
  • applied: false , always. The proposal is for review; nothing is written.

Regenerating source from a graph does not preserve free-text comments. When the base has comments, warnings says so and points you to applyEdits , the comment-preserving path.

Edit: patch the source in place, comments intact

applyEdits applies field-level edits directly to the .intent source, touching only the target lines. Comments, formatting, stable ids, and every untouched block stay byte-identical, and the result comes out already intent fmt-clean:

import { applyEdits } from "@skillstech/intentlang/core";

const result = applyEdits(source, [
  { op: "setField", field: "goal", value: "Create an approved invoice, exactly once." },
  { op: "addGuarantee", statement: "an order is invoiced at most once", verify: "idempotency test" },
  { op: "addField", section: "input", name: "idempotencyKey", type: "IdempotencyKey" },
  { op: "removeNever", match: "payment token" },
]);
// -> { schema: "intent-patch-v1", source, applied, skipped }

Supported operations:

Op Effect
setField Replace the body of goal / why / problem (creating it if absent).
addGuarantee Insert a guarantee block (with optional because and verify).
removeGuarantee Remove the guarantee whose statement matches.
addNever / removeNever Add or remove a never rule.
addField Add a typed field to input / output (creating the block if absent).
removeField Remove a field , and its indented modifiers, so no orphans are left.
setFieldType Change a field's type in place.
addMetric / removeMetric Add or remove a metric block (with optional baseline/target/window).
setMetricField Set a metric's baseline / target / window (inserting the line if absent).
addOutcome / removeOutcome Add or remove an outcome (with an optional description).
addRule / removeRule Add a decision rule (before default) or remove one by name.
setRule Edit a rule's when / return in place.
setDefault Set the decision's default return (creating the default block if absent).

Nothing is applied blindly: an edit that matches nothing, has a bad section, or is missing arguments lands in skipped with a reason, and the rest still apply.

From the command line

The same patcher is a CLI command, so scripts and CI can edit intent without the library:

intent edit mission.intent --set-goal "Create an approved invoice, once." \
  --add-guarantee "an order is invoiced at most once" --write

# or drive it with the full JSON edit list (from a file or stdin)
echo '[{"op":"addField","section":"input","name":"age","type":"int"}]' \
  | intent edit mission.intent --edits -

Without --write it prints the edited source to stdout; with --write it applies in place and reports how many edits applied and were skipped.

The loop

Together the three close the Human ↔ Structured ↔ IntentLang loop with IL as the source of truth: read with parseToStructured, propose graph-level changes with proposeIntent (diffed, never silent), and apply field edits with applyEdits (lossless, comments kept). The sync contract is intent-sync-v1; the patch contract is intent-patch-v1. Both are pre-1.0 and version independently.