Skip to content

Parameter extraction: take: param in forge unify, ingredient param defaults - #14

Merged
llima merged 17 commits into
mainfrom
feat/param-extraction
Sep 26, 2026
Merged

llima merged 17 commits into
mainfrom
feat/param-extraction

Conversation

@llima

@llima llima commented Sep 26, 2026

Copy link
Copy Markdown
Owner

Summary

  • Version bumped to 0.5.0 (minor). Merging publishes craftar@0.5.0 to npm after approval in the npm environment.
  • Parameter extraction (M1 slice 5, spec 09): a forge unify plan hunk may be take: param. Each listed token becomes {{key}} in the base, the base's text is declared as the key's default in its ingredient.yaml, and the variant's text goes into the variant profile's profile.yaml params — only after an equivalence proof shows both sides render back exactly, and only when the same plan resolves the variant. --save-plan pre-fills params for value hunks.
  • New resolve layer: an ingredient's declared params defaults are the weakest layer, scoped to that ingredient (paramsFor).
  • Twenty refusals (P1–P20), all with the Forge untouched: unpairable hunks, uncovered or whitespace-only differences, placeholder changes, conflicting values, reserved keys, a variant another profile uses, a key another recipe/profile/ingredient already sets or cites, files a target copies raw, hand-formatted or aliased YAML. The profile file joins the held-by-git set and the late-failure recovery.
  • Fix: substitute resolves own properties only — {{constructor}}, {{toString}}, {{__proto__}} no longer render built-in JavaScript members.

Emitted bytes — which workspaces see a change

  • Only a workspace whose body contains {{constructor}}, {{toString}}, {{__proto__}} (or another Object.prototype name) sees update: that placeholder now stays literal (commit 8e259be, user-approved).
  • No other byte moves: no Forge that loads under 0.4.0 can hold ingredient params, so paramsFor returns exactly the old params map. Old goldens (forge-unify, acme-portal, acme-web) are unchanged.
  • After an extraction, workspaces on either profile render exactly what they rendered before (the two-profile proof below).

Upgrade note

README § Upgrading → to 0.5.0: a Forge edited with take: param needs 0.5.0 in every workspace that syncs it (0.4.0 refuses ingredient params); import is not template-aware yet (re-importing over an extracted base creates a variant and drops the profile's params — unify warns, W2); the substitute fix above.

Test plan

  • npm run typecheck — passes
  • npm run build — passes
  • npx vitest run --exclude test/ci.test.ts (Linux) — 445 passed / 5 skipped
  • New golden Forge test/golden/forge-param (LF, and CRLF + BOM) reproduces its expected tree; a fresh --save-plan reproduces the committed plans
  • Two-profile sync proof: acme and globex workspaces synced before, every file unchanged byte for byte after both extractions across claude-code, kiro and agents-md; negative control without the profile values shows update
  • Oracle skipped — no fixture on this machine (not using a client workspace); the two-profile proof is the evidence for the new behaviour
  • CI green on Linux and Windows (Windows not run locally; the late-failure test is skipIf(win32 || root))
  • node-cli-reviewer and docs-author: clean after two correction rounds (reuse case refused by P16, mixed reused+literal key skipping P15/P16, --json written/params, profile lookup by real directory, emitter regexes shared, missing refusal tests)
  • Deferred nits: core imports the text-extension regexes from the emitters; the line-level whitespace message is skipped on lines with a reused key (the file-level proof still refuses); the — already in place text marker has no test of its own

An ingredient may declare params with a scalar default, and a plan hunk may be
take: param with its params; the engine refuses param until it can apply it.
…m layer

An ingredient's declared defaults fill only its own placeholders, under every recipe,
profile and workspace layer; a Forge without declarations renders exactly as before.
substitute used `key in params`, so {{constructor}} rendered as the native Object function.
Byte change: a body holding {{constructor}}, {{toString}} or {{__proto__}} now keeps it literal.
Parameter extraction replaces a region by position, so the word diff now carries each
region's character span; classifyHunk's output does not change.
A take: param hunk turns each changed region into {{key}} by position, carries the base's
text as the default and the variant's as the value, pre-fills from the value suggestion,
and refuses anything the equivalence proof could not reproduce.
…xtraction

The rows only the whole Forge can decide (the profile exists, no other profile uses the
variant, no layer or ingredient already holds the key), then both YAML edits rendered in
place, refused unless the file round-trips and reads back as exactly the new entries.
Declarations, template and profile values are written in an emission-neutral order after
every gate passed; --json gains params and profileEdited, and two warnings name what unify
cannot check from inside the Forge.
…tion

import compares literal workspace text with the templated base and creates a variant;
template-aware import is the next slice.
…profiles unchanged

A golden Forge with two extractions (LF and CRLF + BOM) reproduces its expected tree, and two
workspaces synced before, one per profile, stay unchanged byte for byte; a control without the
profile values shows update.
The forge unify row, the ingredient params and their place in the layer order, and the
0.5.0 upgrade notes (older CLIs, re-import, the substitute fix); --help says the same.
P15 and P16 refused every reuse, because the first extraction's profile holds a value; for
a reused key the base already renders {{key}}, so no layer it sees moves. Profiles are found as
loadForge finds them: real directories only, never a symlinked one it would not read.
…params

The --json contract of spec 09 section 4.5: written lists every base file this run wrote, and
params is [] when every key was already declared and valued.
A copied regex would go stale silently if an emitter narrowed its set; the emitters now own
them. Byte-neutral: the patterns are unchanged.
Reuse of an extracted key, P3, P7, P10 on metadata, P20, the alias refusal, ignored and
untracked profiles, a late failure naming the profile, comments and BOM kept, and a mixed plan
seen from a base workspace. The README forge unify row now says unify edits a profile.
A reused site merged first kept the key marked reused even when a later site put new text
under it, so P15/P16 and W1 were skipped and another profile's text could change silently.
The text report now agrees with --json params, which lists only the keys this run wrote.
@llima
llima merged commit cbefcca into main Sep 26, 2026
8 checks passed
@llima
llima deleted the feat/param-extraction branch September 26, 2026 19:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant