@noir-ai/create 1.14.0-beta.1 → 1.15.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -19,7 +19,7 @@ declare function writeAncestors(root: string, map: Record<string, string>): void
19
19
  * adaptation (where to drop `.claude/` vs `.cursor/` etc., future), ignore-file
20
20
  * selection, and the onboarding TUI's confirm step.
21
21
  *
22
- * Design rules (spec §4.5):
22
+ * Design rules:
23
23
  * - Never throws. A foreign/empty dir returns `{ languages: [], monorepo:
24
24
  * false, frameworks: [], packageManager: null }`.
25
25
  * - Never opens network, never parses code beyond a `package.json`/`pyproject`
@@ -41,18 +41,18 @@ interface StackInfo {
41
41
  /** `pnpm` | `npm` | `yarn` when a lockfile is present, else null. */
42
42
  packageManager: string | null;
43
43
  /**
44
- * c4-project-discovery S1: source of the resolved PM. `packageManager-field`
44
+ * Source of the resolved PM. `packageManager-field`
45
45
  * (package.json#packageManager), `lockfile`, `user-agent` (invoke-time
46
46
  * npm_config_user_agent), or `unknown`.
47
47
  */
48
48
  pmSource: 'packageManager-field' | 'lockfile' | 'user-agent' | 'unknown';
49
49
  /**
50
- * c4-project-discovery S3: detected CI. `github`/`gitlab`/`circleci`/
50
+ * Detected CI. `github`/`gitlab`/`circleci`/
51
51
  * `jenkins`/`none`.
52
52
  */
53
53
  ci: string | null;
54
54
  /**
55
- * c4-project-discovery S2: existing AI instruction files found under root.
55
+ * Existing AI instruction files found under root.
56
56
  * Never clobbered — the scaffold confirms the write strategy (skip/add-section/
57
57
  * standalone).
58
58
  */
@@ -66,15 +66,154 @@ interface StackInfo {
66
66
  declare function detectStack(root: string): StackInfo;
67
67
 
68
68
  /**
69
- * The three-mode writer — generalizes keystone-K's `writeManagedRegion` into
69
+ * Template history — the evidence needed to decide whether a shipped seed
70
+ * file may be refreshed during an upgrade.
71
+ *
72
+ * WHY THIS EXISTS
73
+ * ---------------
74
+ * `noir init` writes two files whose only job is to be read by a human:
75
+ * `.noir/.env.example` (documents the variable set) and `.noir/rules/RULES.md`
76
+ * (the AI working-contract seed). Both are emitted create-only-if-absent, and
77
+ * that is normally the right contract: once a file exists, the user owns it and
78
+ * Noir must never overwrite their edits.
79
+ *
80
+ * It has one bad consequence. When a later release adds a variable to
81
+ * `.noir/.env.example`, an already-initialized project keeps the old text
82
+ * forever — the upgrade path cannot tell "the user rewrote this file" apart
83
+ * from "the user never touched it, and the shipped text has moved on". Those
84
+ * two need opposite handling: leave the first alone, refresh the second. A
85
+ * plain-content check cannot distinguish them, and nothing on disk records the
86
+ * bytes that were originally written.
87
+ *
88
+ * This module supplies the missing evidence. It holds the exact bytes of the
89
+ * seed templates as they shipped at each past scaffold version, so a caller can
90
+ * ask: "are the bytes on disk identical to some version Noir once wrote, and
91
+ * different from what Noir writes today?" If yes, the file is an unedited
92
+ * leftover and refreshing it loses nothing.
93
+ *
94
+ * THE CONTRACT
95
+ * ------------
96
+ * {@link isStaleSeed} answers exactly that question. It is deliberately
97
+ * conservative in one direction: it returns `true` only on an exact match
98
+ * against a recorded past version, so ANY other byte difference — a user edit,
99
+ * a re-wrapped line, even a CRLF line-ending conversion from a Windows
100
+ * checkout — reads as "user-owned" and the file is left untouched. A missed
101
+ * refresh is a documentation gap; a wrong refresh destroys user work. The
102
+ * asymmetry is intentional and should not be "fixed" into a fuzzy match.
103
+ *
104
+ * Each entry holds the text of BOTH seeds, so the comparison takes the seed it
105
+ * is about as an argument and scans only that field. Scanning all of them would
106
+ * let a `.env.example` match the recorded working-rules text (or the reverse),
107
+ * and since the caller's response to `true` is an overwrite, a mixed-up pairing
108
+ * would discard an edit the user is entitled to keep. The caller knows which
109
+ * file it just read; the argument makes it say so.
110
+ *
111
+ * RENDERING
112
+ * ---------
113
+ * Seed templates are normally interpolated through `render(template, vars)`
114
+ * before writing. That step is a no-op for the two templates recorded here:
115
+ * neither contains a `{{...}}` token, so `render(text, vars) === text` for
116
+ * every possible `vars`. The registry therefore stores raw template text and
117
+ * {@link isStaleSeed} compares it against raw file bytes — no `vars` argument
118
+ * is needed, and callers do not have to reconstruct the context that wrote the
119
+ * file years ago.
120
+ *
121
+ * That equivalence is load-bearing, so it is asserted rather than merely
122
+ * documented: `template-history.test.ts` fails if any recorded template
123
+ * contains a `{{` token. A future seed template that DOES interpolate needs
124
+ * `render` applied to the history entry (and a `vars` parameter on the
125
+ * comparison) before it is added here — the test exists to make that
126
+ * requirement impossible to overlook.
127
+ *
128
+ * APPENDING TO THE HISTORY
129
+ * ------------------------
130
+ * Record the template bytes as they shipped, keyed by the scaffold version
131
+ * whose `init` wrote them, and never edit an existing entry — a user's disk
132
+ * still holds those old bytes, and rewriting history would silently reclassify
133
+ * their file as "current" (no refresh) or "unknown" (no refresh). The same
134
+ * reasoning that keeps npm versions immutable applies here.
135
+ */
136
+ /**
137
+ * Which recorded seed a comparison is about. The values mirror the field names
138
+ * of {@link SeedTemplateHistoryEntry}: every entry carries the text of both
139
+ * seeds, and a caller must name the one whose bytes it read (see
140
+ * {@link isStaleSeed} on why the pairing is load-bearing).
141
+ */
142
+ type SeedKind = 'envExample' | 'rulesSeed';
143
+ /**
144
+ * The seed templates as they shipped at one past scaffold version.
145
+ *
146
+ * Only the doc-only seeds are tracked. Files the user is expected to edit
147
+ * heavily (`.noir/config.yml`, `.noir/project.id`) are not candidates for
148
+ * refresh at all, and co-owned files that carry a managed region already have a
149
+ * three-way merge path of their own.
150
+ */
151
+ interface SeedTemplateHistoryEntry {
152
+ /**
153
+ * Bare `x.y.z` scaffold version whose `init` wrote these bytes — the value
154
+ * stamped into `.noir/scaffold-version` at the time, with no `v` prefix and
155
+ * no pre-release suffix.
156
+ *
157
+ * Spelled out literally rather than read from `CURRENT_SCAFFOLD_VERSION`:
158
+ * the version recorded here is a historical fact about these bytes, so it
159
+ * must not advance when the current version does.
160
+ */
161
+ readonly scaffoldVersion: string;
162
+ /** Raw text of the `env.example` template at this version (see the module
163
+ * header on why raw text equals the rendered file for this template). */
164
+ readonly envExample: string;
165
+ /** Raw text of the `rules-seed.md` template at this version. */
166
+ readonly rulesSeed: string;
167
+ }
168
+ /**
169
+ * The recorded seed-template bytes, oldest first.
170
+ *
171
+ * Read-only by intent: this is an append-only ledger of shipped bytes, not
172
+ * mutable configuration. Entries are never edited or removed (see the module
173
+ * header on why rewriting a past entry would silently misclassify a user's
174
+ * file), so a version that changed a seed template appends rather than
175
+ * updates.
176
+ */
177
+ declare const SEED_TEMPLATE_HISTORY: readonly SeedTemplateHistoryEntry[];
178
+ /**
179
+ * Does `fileBytes` look like an unedited `seed` left over from an older scaffold
180
+ * version?
181
+ *
182
+ * `true` iff the bytes are an exact match for that seed's text in a recorded
183
+ * history entry AND differ from `currentRender` (what Noir writes for that seed
184
+ * today). Callers pass `currentRender` rather than this module deriving it,
185
+ * because the current template text is the business of the manifest and the
186
+ * template loader — and because the compile-time constant that names the
187
+ * current scaffold version advances independently of the bytes recorded here.
188
+ *
189
+ * `seed` names the file the bytes came from. It is a safety argument, not
190
+ * bookkeeping: because each entry records both seeds, a comparison that scanned
191
+ * every field would report the recorded working-rules bytes as a stale
192
+ * `.env.example`, and the caller's response to `true` is to overwrite the file.
193
+ * Naming the seed keeps the comparison inside its own column, so a mixed-up
194
+ * pairing cannot silently destroy an edit the user is entitled to keep.
195
+ *
196
+ * The `currentRender` guard is checked FIRST: it is what keeps a no-op from
197
+ * being reported as a refresh. A project initialized at the current version
198
+ * already holds the current bytes; those bytes also match their own history
199
+ * entry, so without the second condition every up-to-date project would be told
200
+ * to rewrite its seeds.
201
+ *
202
+ * Callers own the other half of the decision: a file that does not exist has no
203
+ * bytes to compare and is a plain "create the seed", not a refresh.
204
+ */
205
+ declare function isStaleSeed(seed: SeedKind, fileBytes: string, currentRender: string): boolean;
206
+
207
+ /**
208
+ * The three-mode writer — generalizes the earlier `writeManagedRegion` into
70
209
  * the declarative dispatch the scaffold manifest drives. Each mode maps 1:1 to
71
- * an artifact class in the spec §4.5 matrix:
210
+ * an artifact class in the artifact matrix:
72
211
  *
73
212
  * - {@link regenerate} — pure pointers (`.mcp.json`, `NOIR.md` brief, …).
74
213
  * Always overwritten, atomically.
75
214
  * - {@link managedBlock} — co-owned files (`CLAUDE.md` context/rules,
76
215
  * `.gitignore` noir block, …). DELEGATES to
77
- * keystone-K's `writeManagedRegion` so user content
216
+ * core's `writeManagedRegion` so user content
78
217
  * outside the markers is preserved byte-for-byte and
79
218
  * re-runs are idempotent. Never duplicate the
80
219
  * managed-region logic.
@@ -82,7 +221,7 @@ declare function detectStack(root: string): StackInfo;
82
221
  * `project.id`). Write once; never clobber.
83
222
  *
84
223
  * The orchestrator (`scaffold.ts`) is the only intended caller; the per-mode
85
- * functions are exported so the cli (S-T2) and tests can drive them directly
224
+ * functions are exported so the cli and tests can drive them directly
86
225
  * when a one-off write is needed outside the manifest.
87
226
  */
88
227
  type WriteMode = 'regenerate' | 'managedBlock' | 'skipIfExists' | 'mergeJson';
@@ -104,7 +243,7 @@ interface WriteOutcome {
104
243
  * for the whole manifest so a missing dir is a single, attributable failure
105
244
  * rather than N silent ones inside the writer. */
106
245
  declare function regenerate(absPath: string, content: string): WriteOutcome;
107
- /** Re-emit a managed region, delegating to keystone-K's `writeManagedRegion`.
246
+ /** Re-emit a managed region, delegating to core's `writeManagedRegion`.
108
247
  * `regionText` MUST already include the begin/end markers (matches the shape
109
248
  * `writeManagedRegion` expects and that `IGNORE_BLOCK`/`CONTEXT_BLOCK`
110
249
  * callers build in core/cli). Use {@link buildRegion} to assemble it from a
@@ -120,12 +259,12 @@ declare function managedBlock(absPath: string, block: ManagedBlock, regionText:
120
259
  * conventional trailing newline in `.tmpl` files without producing a
121
260
  * double-newline before the end marker. This keeps the output byte-identical
122
261
  * to `claudeAdapter.emitContext`/`emitRules` and core's `syncIgnores`, which
123
- * S-T2 relies on for a diff-free refactor. */
262
+ * the cli refactor relies on for diff-free output. */
124
263
  declare function buildRegion(block: ManagedBlock, body: string): string;
125
264
  /** Write `content` to `absPath` only if no file exists there. Returns whether
126
265
  * bytes were written. Parent dirs are created by {@link atomicWriteFile}.
127
266
  *
128
- * `fileMode` (slice E, §8.1) is the permission for the NEWLY created file —
267
+ * `fileMode` is the permission for the NEWLY created file —
129
268
  * `0o600` for the `.noir/.env` credential seed. It is applied to the temp file
130
269
  * BEFORE the rename (see `AtomicWriteOptions.mode`), so the file never exists
131
270
  * at its final path with a laxer mode. It is deliberately ignored when the
@@ -146,12 +285,13 @@ declare function skipIfExists(absPath: string, content: string, fileMode?: numbe
146
285
  * write {@link WriteMode} so the orchestrator can dispatch without knowing
147
286
  * what's inside.
148
287
  *
149
- * FAITHFULNESS CONTRACT (S-T1 → S-T2 → S10): this table is a strict superset of
150
- * the artifacts `packages/cli/src/{init,sync}.ts` wrote pre-Slice-S. The cli
151
- * refactor (S-T2) replaced those ad-hoc writers with a call into `scaffold()`;
152
- * the byte-for-byte output MUST stay equivalent for first-run init. S10 makes
153
- * the manifest HOST-PARAMETRIC: {@link buildManifest} now returns host-agnostic
154
- * entries + a {@link buildHostArtifacts} call that materializes per-host files
288
+ * FAITHFULNESS CONTRACT: this table is a strict superset of
289
+ * the artifacts `packages/cli/src/{init,sync}.ts` wrote before the manifest
290
+ * refactor. The cli refactor replaced those ad-hoc writers with a call into
291
+ * `scaffold()`; the byte-for-byte output MUST stay equivalent for first-run
292
+ * init. The manifest is HOST-PARAMETRIC: {@link buildManifest} returns
293
+ * host-agnostic entries + a {@link buildHostArtifacts} call that materializes
294
+ * per-host files
155
295
  * (CLAUDE.md/GEMINI.md for claude/gemini; AGENTS.md + .cursor/.../opencode.json
156
296
  * for agents-md/cursor/opencode) via the resolved adapter. The claude default
157
297
  * `noir init` stays BYTE-IDENTICAL to v1.1 — REMOVED the additive
@@ -163,15 +303,36 @@ declare function skipIfExists(absPath: string, content: string, fileMode?: numbe
163
303
  * `join(root, entry.path) === paths.X(root)` for every entry layout knows
164
304
  * about, so a layout rename is caught here instead of silently drifting.
165
305
  */
166
- /** S10: `HostTag` is now the SAME `HostId` enum the adapter registry uses
167
- * (re-exported so existing imports keep working). Pre-S10 this was the
168
- * literal `'claude'`; widening to `HostId` lets one manifest serve every host
169
- * via the orchestrator's host filter + {@link buildHostArtifacts}. */
306
+ /** `HostTag` is the SAME `HostId` enum the adapter registry uses
307
+ * (re-exported so existing imports keep working); it used to be the
308
+ * literal `'claude'`. Widening it to `HostId` lets one manifest serve every
309
+ * host via the orchestrator's host filter + {@link buildHostArtifacts}. */
170
310
  type HostTag = HostId;
171
311
  interface ManifestEntry {
172
312
  /** Repo-relative POSIX path (forward slashes). Orchestrator joins with root. */
173
313
  path: string;
174
314
  mode: WriteMode;
315
+ /** Opt-in flag for a DOC-ONLY seed — a file whose only job is to be read by a
316
+ * human. On an upgrade, a seed still byte-identical to what an older Noir
317
+ * shipped is refreshed in place rather than staying frozen at that older
318
+ * text forever; one the user edited is never refreshed (it goes to the
319
+ * ordinary conflict flow, which keeps the user's bytes by default).
320
+ *
321
+ * Honored on the upgrade emit only. A fresh `init`/`create` either creates
322
+ * the file or leaves it alone, and `sync` does not emit seeds at all, so
323
+ * neither can observe the flag — the behavior change is confined to the one
324
+ * command a user runs to bring an initialized project up to date.
325
+ *
326
+ * DOC-ONLY SEEDS ONLY. A file the user owns the contents of must never carry
327
+ * this flag: `.noir/.env`, `.noir/config.yml` and `.noir/project.id` are
328
+ * theirs to shape, and an upgrade that rewrote one of them would destroy
329
+ * exactly the work this engine exists to protect. Co-owned files (managed
330
+ * blocks) must not set it either — they have a three-way merge that
331
+ * preserves user edits, which is the better tool for a shared file.
332
+ *
333
+ * Which recorded seed the bytes are compared against is declared separately
334
+ * in {@link REFRESHABLE_SEED_KIND}. */
335
+ refreshIfStale?: true;
175
336
  /** Host tag; entry is skipped when opts.host !== entry.host.
176
337
  * Undefined = host-agnostic (every host emits it). */
177
338
  host?: HostTag;
@@ -202,7 +363,7 @@ interface ManifestEntry {
202
363
  dedupSubstring?: string;
203
364
  }
204
365
  type BuildManifestContext = {
205
- /** Absolute repo root. Added in S10 so {@link buildHostArtifacts} can resolve
366
+ /** Absolute repo root. Needed so {@link buildHostArtifacts} can resolve
206
367
  * absolute adapter paths (`adapter.mcpConfigPath({root})`, etc.) to the
207
368
  * manifest's repo-relative POSIX shape. */
208
369
  root: string;
@@ -226,7 +387,7 @@ type BuildManifestContext = {
226
387
  stack?: StackInfo;
227
388
  };
228
389
  /** Co-owned NOIR.md auto-brief region. Defined locally (not exported from
229
- * core) because core's keystone-K named instances cover only the three
390
+ * core) because core's own named block instances cover only the three
230
391
  * regions core itself writes (context/rules/ignore); the brief is the
231
392
  * scaffold engine's own. Uses the SAME `managedBlock()` factory so marker
232
393
  * shape stays consistent with the rest of the family. */
@@ -239,14 +400,14 @@ declare const MANIFEST_PATH_PARITY: ReadonlyArray<[
239
400
  * Build the manifest for a given ctx. Pure (no I/O). The orchestrator calls
240
401
  * this once per scaffold run; tests assert the shape is stable.
241
402
  *
242
- * S10 structure: the manifest is now `[...hostAgnosticEntries(ctx), ...hostSpecificEntries(ctx)]`
403
+ * The manifest is `[...hostAgnosticEntries(ctx), ...hostSpecificEntries(ctx)]`
243
404
  * where the host-specific half comes from {@link buildHostArtifacts} (driven by
244
405
  * `resolveAdapter(ctx.host)`). The host-agnostic half is unchanged from v1.1
245
406
  * (canonical `.noir/` store + ignore files). {@link buildHostArtifacts}
246
407
  * emits AGENTS.md ONLY for agents-md/cursor/opencode (claude/gemini use their
247
408
  * own CLAUDE.md/GEMINI.md — emitting AGENTS.md too would double-import .noir/).
248
409
  *
249
- * Mode-tagging rationale per artifact (see S-T1 report for the full table):
410
+ * Mode-tagging rationale per artifact:
250
411
  * - `project.id` → skipIfExists. First init writes a fresh id; re-init MUST
251
412
  * NOT overwrite — that would orphan the indexed store DB named after it.
252
413
  * - `config.yml` → skipIfExists. User-owned; the seed is written once.
@@ -374,7 +535,7 @@ declare function mergeThreeWay(base: string, ours: string, theirs: string, style
374
535
  /**
375
536
  * Migration model — Nx-style declarative upgrade scripts, hand-rolled.
376
537
  *
377
- * Per the Slice S design (S-OQ2 resolved → inline conflict markers, never
538
+ * Per the migration design (inline conflict markers, never
378
539
  * interactive prompts), every migration script:
379
540
  * - is a small, IDEMPOTENT TypeScript function (`run(root, opts) → result`).
380
541
  * Re-running it on an already-migrated tree is a no-op (or produces the
@@ -385,10 +546,10 @@ declare function mergeThreeWay(base: string, ours: string, theirs: string, style
385
546
  * `result.conflicts` so the caller (doctor/CI) can fail loudly with a
386
547
  * reviewable artifact rather than hang on a prompt.
387
548
  *
388
- * The registry ({@link MIGRATIONS}) carries two entries: the synthetic
389
- * `1.0.0 → 1.0.0` smoke test that exercises the runner end-to-end, and the
390
- * first real version-to-version migration, `1.0.0 → 1.1.0`. The synthetic one
391
- * is the copy-pasteable template for the next contributor.
549
+ * The registry ({@link MIGRATIONS}) carries three entries: the synthetic
550
+ * `1.0.0 → 1.0.0` smoke test that exercises the runner end-to-end, and two real
551
+ * version-to-version migrations, `1.0.0 → 1.1.0` and `1.1.0 → 1.2.0`. The
552
+ * synthetic one is the copy-pasteable template for the next contributor.
392
553
  */
393
554
  interface MigrationContext {
394
555
  /** Absolute repo root the migration operates on. */
@@ -447,8 +608,8 @@ declare function runMigrations(root: string, from: string | null, to: string, op
447
608
  * order is documentation only — oldest step first. */
448
609
  declare const MIGRATIONS: readonly MigrationScript[];
449
610
  /** Write git-style inline conflict markers around `theirs`/`ours` so a human
450
- * or AI agent can resolve later. This is the CI-safe fallback the spec locks
451
- * in (S-OQ2) — no interactive prompts, ever. */
611
+ * or AI agent can resolve later. This is the CI-safe fallback —
612
+ * no interactive prompts, ever. */
452
613
  declare function applyInlineConflict(ours: string, theirs: string, oursLabel?: string, theirsLabel?: string): string;
453
614
  /** Apply `(ours, theirs)` to a region: if they're equal, return `ours` (no
454
615
  * conflict); otherwise emit inline markers. Migration authors should prefer
@@ -461,7 +622,7 @@ declare function applyWithConflict(ours: string, theirs: string, path: string):
461
622
  };
462
623
 
463
624
  /**
464
- * Scaffold orchestrator. One function the cli (S-T2) calls for `noir init`,
625
+ * Scaffold orchestrator. One function the cli calls for `noir init`,
465
626
  * `noir create`, and `noir sync`; mode selects the manifest subset + how
466
627
  * project identity is resolved.
467
628
  *
@@ -500,19 +661,19 @@ interface ScaffoldOptions {
500
661
  * initialization without ever touching a file the user owns. Only meaningful
501
662
  * with mode `'init'`. */
502
663
  upgrade?: boolean;
503
- /** SP-A: re-scaffold even when the target is already initialized (bypasses
664
+ /** Re-scaffold even when the target is already initialized (bypasses
504
665
  * the already-initialized no-op guard). Does NOT bypass `assertSafeRoot`
505
666
  * — root-safety is hard, never bypassable. */
506
667
  force?: boolean;
507
668
  /** Preview: compute the same written/skipped/migrated lists without touching
508
669
  * disk. `noir doctor`/CI use this to report drift. */
509
670
  dryRun?: boolean;
510
- /** SP-C: policy for a `regenerate` file that exists and DIFFERS from the
671
+ /** Policy for a `regenerate` file that exists and DIFFERS from the
511
672
  * template, when no {@link onConflict} callback is provided. Default
512
673
  * `'overwrite'` is byte-backward-compatible. `'preserve'` keeps the user's
513
674
  * file (the non-TTY / CI default in the cli). */
514
675
  conflictPolicy?: 'overwrite' | 'preserve';
515
- /** SP-C: per-file conflict resolver — the UI seam (the engine stays UI-free;
676
+ /** Per-file conflict resolver — the UI seam (the engine stays UI-free;
516
677
  * the cli injects a @clack-based resolver). Called when a `regenerate`
517
678
  * file exists and differs from the template. The return type widens to
518
679
  * accept a rich shape carrying `applyToAll` — the engine then reuses the
@@ -525,7 +686,8 @@ interface ScaffoldOptions {
525
686
  * run always has a base. Set to `false` (CLI `--no-merge-regions`) to
526
687
  * restore strip-replace. Supports BOTH single-region (NOIR.md, ignores) AND
527
688
  * multi-region (CLAUDE.md CONTEXT+RULES) managed files — the multi-region
528
- * path shipped with SP-H (`managedBlocks` + `mergeManagedRegion` per block). */
689
+ * path merges managed regions per block (`managedBlocks` +
690
+ * `mergeManagedRegion`). */
529
691
  mergeManagedRegions?: boolean;
530
692
  /** Explicit interactivity signal. The engine reads THIS (not
531
693
  * `process.env`), so a direct API/embedded caller in a TTY that does NOT
@@ -539,10 +701,24 @@ interface ScaffoldResult {
539
701
  written: string[];
540
702
  /** Repo-relative paths skipIfExists'd (already present). */
541
703
  skipped: string[];
542
- /** SP-D: repo-relative `regenerate` paths skipped because byte-identical to
704
+ /** Repo-relative `regenerate` paths skipped because byte-identical to
543
705
  * the template (content-hash dedup — no rewrite). */
544
706
  identical: string[];
545
- /** SP-A: true when the already-initialized guard short-circuited (a bare
707
+ /** Repo-relative doc-only seed paths REPLACED on the upgrade path because the
708
+ * copy on disk was still byte-identical to an older Noir's shipped text —
709
+ * i.e. the user never edited it, so a newer seed costs nothing. Reported
710
+ * separately from {@link written} because the distinction is the whole point
711
+ * of the refresh: a caller (and the user reading the summary) can tell
712
+ * "Noir updated documentation you never touched" apart from "Noir created
713
+ * something new".
714
+ *
715
+ * A seed the user DID edit is never listed here — it goes through the
716
+ * ordinary conflict flow, so it appears in {@link written} only when a
717
+ * resolver explicitly chose to replace it (or `--force` did). Empty on every
718
+ * other run: a fresh `init`/`create` has nothing to compare against and
719
+ * `sync` does not emit seeds. */
720
+ refreshed: string[];
721
+ /** True when the already-initialized guard short-circuited (a bare
546
722
  * `noir init`/`create` on an initialized project). Callers (init.ts/create.ts)
547
723
  * gate skills emission + the "initialized" message on this — a no-op must NOT
548
724
  * re-emit skills or claim it initialized. */
@@ -551,6 +727,10 @@ interface ScaffoldResult {
551
727
  migrationsRan: string[];
552
728
  /** Migration conflicts (repo-relative or `<runner>:…`), when upgrade ran. */
553
729
  migrationConflicts: string[];
730
+ /** Repo-relative paths a migration CHANGED (wrote or refreshed), when upgrade
731
+ * ran — reported separately from {@link refreshed} so callers can name
732
+ * "migrated" files (e.g. `.noir/config.yml`) apart from doc seeds. */
733
+ migrationChanged: string[];
554
734
  stack: StackInfo;
555
735
  projectId: string;
556
736
  fromVersion: string | null;
@@ -589,7 +769,7 @@ interface ConflictRecord {
589
769
  /** Resolution the engine applied (`replace`/`preserve`/`rename`/…). */
590
770
  resolution: ConflictResolution;
591
771
  }
592
- /** SP-C — context passed to {@link ScaffoldOptions.onConflict} when a
772
+ /** Context passed to {@link ScaffoldOptions.onConflict} when a
593
773
  * `regenerate` file exists and differs from the template. */
594
774
  interface ConflictContext {
595
775
  /** Repo-relative path of the conflicting file. */
@@ -600,7 +780,7 @@ interface ConflictContext {
600
780
  proposed: string;
601
781
  /** Artifact class — drives apply-to-all memory scope (`regenerate` shares
602
782
  * one decision across a run; `managedBlock`/`managedBlocks` stay per-file).
603
- * Defaults to `'regenerate'` for backward compatibility with SP-C resolvers
783
+ * Defaults to `'regenerate'` for backward compatibility with older resolvers
604
784
  * that pre-date the field (the engine always sets it). */
605
785
  mode?: 'regenerate' | 'managedBlock' | 'managedBlocks' | 'skipIfExists' | 'skill' | 'markdown' | 'artifact';
606
786
  /** LCS similarity (0-1) between existing and proposed. Cheap signal for
@@ -610,7 +790,7 @@ interface ConflictContext {
610
790
  * bytes WITH zdiff3 conflict markers (selecting `'merge'` writes this). */
611
791
  mergedWithMarkers?: string;
612
792
  }
613
- /** SP-C — how to resolve a `regenerate` conflict. */
793
+ /** How to resolve a `regenerate` conflict. */
614
794
  type ConflictResolution = 'replace' | 'preserve' | 'rename' | 'duplicate' | 'cancel' | 'merge';
615
795
  /** The resolver may return a bare {@link ConflictResolution} (scope
616
796
  * `'one'`) OR a rich shape carrying `applyToAll`. The engine unwraps both; when
@@ -623,7 +803,7 @@ type ConflictResolverReturn = ConflictResolution | {
623
803
  };
624
804
  /**
625
805
  * Refuse to scaffold when `root` is — or is inside — a `.noir/` directory.
626
- * (SP-A) Running `noir init`/`create`/`sync` while cwd = `.noir/` would
806
+ * Running `noir init`/`create`/`sync` while cwd = `.noir/` would
627
807
  * otherwise mint a FRESH project id (because `<root>/.noir/project.id` is
628
808
  * absent) and build a NESTED second project (`.noir/.noir/`, `.noir/CLAUDE.md`,
629
809
  * `.noir/.claude/skills/`, …) — the duplicate-`.noir` bug. The walk ascends the
@@ -652,7 +832,7 @@ declare function scaffold(opts: ScaffoldOptions): Promise<ScaffoldResult>;
652
832
  */
653
833
  /** The scaffold version this build of @noir-ai/create ships. Bumped atomically
654
834
  * whenever a manifest entry, template, or migration changes shape. */
655
- declare const CURRENT_SCAFFOLD_VERSION = "1.1.0";
835
+ declare const CURRENT_SCAFFOLD_VERSION = "1.2.0";
656
836
  /** Path to the stamp file under `root`. Exposed for tests + doctor. */
657
837
  declare function scaffoldVersionPath(root: string): string;
658
838
  /** Read the applied scaffold version, or `null` if the stamp is absent/unparseable.
@@ -662,7 +842,7 @@ declare function readScaffoldVersion(root: string): string | null;
662
842
  * LAST so a crash mid-scaffold leaves an old/absent stamp rather than a
663
843
  * misleading fresh one.
664
844
  *
665
- * N2: routed through the package's atomic `regenerate()` writer (tmp+rename in
845
+ * Routed through the package's atomic `regenerate()` writer (tmp+rename in
666
846
  * the same dir) for consistency with the rest of the engine's durable writes —
667
847
  * a half-written stamp would mislead `noir doctor`/`init --upgrade`, so the
668
848
  * stamp deserves the same crash-atomicity as `.mcp.json` and the NOIR.md brief. */
@@ -698,4 +878,4 @@ declare function loadTemplate(name: string): string;
698
878
  /** Resolve a template name to its absolute path (for diagnostics + tests). */
699
879
  declare function templatesDir(): string;
700
880
 
701
- export { BRIEF_BLOCK, type BuildHostArtifactsContext, type BuildManifestContext, CURRENT_SCAFFOLD_VERSION, type ConflictContext, type ConflictRecord, type ConflictResolution, type ConflictResolverReturn, type DiffLine, type HostTag, MANIFEST_PATH_PARITY, MIGRATIONS, type ManifestEntry, type MergeResult, type MigrationContext, type MigrationResult, type MigrationScript, type ScaffoldMode, type ScaffoldOptions, type ScaffoldResult, type StackInfo, type WriteMode, type WriteOutcome, ancestorsPath, applyInlineConflict, applyWithConflict, assertSafeRoot, buildHostArtifacts, buildManifest, buildRegion, detectStack, lineDiff, loadTemplate, managedBlock, mergeThreeWay, readAncestors, readScaffoldVersion, regenerate, render, runMigrations, scaffold, scaffoldVersionPath, skipIfExists, templatesDir, writeAncestors, writeScaffoldVersion, zdiff3Region };
881
+ export { BRIEF_BLOCK, type BuildHostArtifactsContext, type BuildManifestContext, CURRENT_SCAFFOLD_VERSION, type ConflictContext, type ConflictRecord, type ConflictResolution, type ConflictResolverReturn, type DiffLine, type HostTag, MANIFEST_PATH_PARITY, MIGRATIONS, type ManifestEntry, type MergeResult, type MigrationContext, type MigrationResult, type MigrationScript, SEED_TEMPLATE_HISTORY, type ScaffoldMode, type ScaffoldOptions, type ScaffoldResult, type SeedTemplateHistoryEntry, type StackInfo, type WriteMode, type WriteOutcome, ancestorsPath, applyInlineConflict, applyWithConflict, assertSafeRoot, buildHostArtifacts, buildManifest, buildRegion, detectStack, isStaleSeed, lineDiff, loadTemplate, managedBlock, mergeThreeWay, readAncestors, readScaffoldVersion, regenerate, render, runMigrations, scaffold, scaffoldVersionPath, skipIfExists, templatesDir, writeAncestors, writeScaffoldVersion, zdiff3Region };