@sabaiway/agent-workflow-kit 7.4.0 → 7.5.0
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/CHANGELOG.md +32 -0
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/upgrade.md +2 -2
- package/references/scripts/spec-schema.mjs +37 -5
- package/references/scripts/spec-schema.test.mjs +67 -0
- package/tools/ensure-configs.mjs +5 -4
- package/tools/ensure-specs.mjs +22 -13
- package/tools/ensure-vocabulary.mjs +2 -2
- package/tools/script-priors.mjs +8 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,38 @@ Semantically versioned ([semver](https://semver.org)), newest first. The `versio
|
|
|
4
4
|
is the current release. `upgrade` mode reads a project's `docs/ai/.workflow-version` and applies
|
|
5
5
|
every `migrations/<version>-<slug>.md` newer than it, in semver order.
|
|
6
6
|
|
|
7
|
+
## 7.5.0 — the reader pair joins the known-prior refresh lane; the mirrored reader gains the structure verdict (AD-114)
|
|
8
|
+
|
|
9
|
+
Slice 2b's `spec-check` will need a structural read of a spec document, and the reader is the ONE
|
|
10
|
+
definition of well-formed — so the memory canon's reader (mirrored here byte-identically) now
|
|
11
|
+
returns an additive `structure` field. Because the bundled reader bytes move, the reader pair joins
|
|
12
|
+
the AD-113 known-prior refresh lane in the SAME release: without catalog rows for the outgoing
|
|
13
|
+
bodies, every 4.6.x deployment would classify `custom` against the new bundle and be stranded out of
|
|
14
|
+
the store seed forever.
|
|
15
|
+
|
|
16
|
+
- **`tools/script-priors.mjs` — the catalog spans both pairs.** `PRIOR_FILES` is now the FULL
|
|
17
|
+
refreshable catalog (checker pair + reader pair); `SCRIPT_PRIORS` appends the outgoing
|
|
18
|
+
4.6.0..4.6.1 reader-pair bodies (rows 4 -> 6, fixtures under `4.6.0/`, the literal frozen prefix
|
|
19
|
+
extended with its first four rows byte-identical). The maintenance rule generalizes with it: a
|
|
20
|
+
release that changes ANY catalog member appends the outgoing body's row.
|
|
21
|
+
- **`tools/ensure-specs.mjs` — the pairs decoupled, the reader refreshes.** `CHECKER_PAIR` was an
|
|
22
|
+
alias of `PRIOR_FILES`, so the widened catalog would have double-surveyed the readers: the op now
|
|
23
|
+
owns exported `READER_PAIR`/`CHECKER_PAIR` (the suite pins pairs-union === catalog domain), and
|
|
24
|
+
`decideWrites` admits a reader REFRESH — reader seeds stay unconditional create-only (a custom
|
|
25
|
+
neighbour never blocks them); a refresh runs only inside a pair whose every file is
|
|
26
|
+
current | prior | absent; one custom file withholds its pair's REFRESHES and every downstream
|
|
27
|
+
write, DIRECTIONALLY (a prior reader still refreshes beside a custom checker, which holds back
|
|
28
|
+
only its own pair and the store). Order reader -> checker -> store unchanged; lines and tokens
|
|
29
|
+
unchanged (`refreshed` now covers either pair).
|
|
30
|
+
- **Prose caught up at every live surface** — `references/modes/upgrade.md` (the configs summary
|
|
31
|
+
and the spec-layer paragraph), the `ensure-configs` header + help, the vocabulary comment, the
|
|
32
|
+
package-content ledger, and the memory twin's refresh-lane sentence all name BOTH pairs.
|
|
33
|
+
- **Proof.** The state table gains the reader-prior rows, the directional-withhold pin and dry-run
|
|
34
|
+
rows; the E2E upgrades a real 4.6.x deployment (reader on the 4.6.0 bodies, checker current)
|
|
35
|
+
whole under the real installed pre-commit hook, and the whole-chain prior+prior cell stays
|
|
36
|
+
table-covered. Red-first throughout — three red-proofs minted on frozen test bytes against the
|
|
37
|
+
pre-fix modules. Tarball count unchanged at 247.
|
|
38
|
+
|
|
7
39
|
## 7.4.0 — the spec layer reaches existing deployments: known-prior refresh, seed-behind-checker, the `specs` ensure (AD-113)
|
|
8
40
|
|
|
9
41
|
7.3.0 made a FRESH bootstrap deliver the feature-spec layer whole; this release gives every EXISTING
|
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: agent-workflow-kit
|
|
|
3
3
|
description: Deploy or upgrade a portable AI-agent memory-and-workflow system in any project. Use when the user wants to bootstrap `docs/ai/` + an entry-point `AGENTS.md` (+ `CLAUDE.md` alias) + cap/archive/index enforcement in a new or existing repo, set up the Memory Map and session protocols, install the docs-rotation pre-commit hook, or run `/agent-workflow-kit` / `/agent-workflow-kit upgrade`. Triggers on phrases like "set up the memory system", "deploy the AI workflow here", "bootstrap docs/ai", "upgrade the workflow".
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
metadata:
|
|
6
|
-
version: '7.
|
|
6
|
+
version: '7.5.0'
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# agent-workflow-kit
|
package/capability.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sabaiway/agent-workflow-kit",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.5.0",
|
|
4
4
|
"description": "Portable, cross-agent memory & workflow for AI coding agents — Claude Code, Codex, Cursor, Devin Desktop. One command deploys an AGENTS.md entry point + docs/ai context with cap/archive/index enforcement into any repo.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agents",
|
|
@@ -30,7 +30,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
30
30
|
|
|
31
31
|
**`footprint` — hidden-mode footprint reconcile (D9 / AD-014).** A deployment does not record whether it chose `hidden`, so first **infer visibility** — the dry-run writes **zero bytes** and reports one of — **visible** (the entry point is tracked) → nothing to do; **ambiguous** (untracked but not ignored — could be a fresh uncommitted repo, or a hide that broke) → **ASK** the user which it is, never guess; **hidden** → re-run without `--dry-run` to migrate any older **machine-global** hide to the **project-local** `.git/info/exclude` (one managed block; folds in the legacy `.claude/skills/` line), idempotently (a clean re-run is zero-diff). Handle its surfaced paths exactly as bootstrap step 9 (`${CLAUDE_SKILL_DIR}/references/modes/bootstrap.md`) (already-committed → show `git rm --cached`, ask before `--include`; generic-name present file → ask; **leftover machine-wide ignore block → ASK before `--remove-global`**, default keep + report). No Node on the agent host / Windows → as bootstrap step 9 (`${CLAUDE_SKILL_DIR}/references/modes/bootstrap.md`). This runs on **every** hidden upgrade, like the methodology slot — no lineage-head bump, no migration file.
|
|
32
32
|
|
|
33
|
-
**`configs` — the project-configuration ensures, ONE run.** The ONE command performs **all six** ensures described below — orchestration config · gate declaration · autonomy declaration · enforcement scripts · spec layer · navigator index — in a fixed order, and prints **one outcome line per ensure**: paste those lines into the step 4 / step 8 success report. Every SEED is **create-only** (an existing file is preserved byte-for-byte); the three refresh-class ops are named apart — the orchestration onboarding note (refreshed only while it still matches a canonical this kit shipped), the spec-layer checker
|
|
33
|
+
**`configs` — the project-configuration ensures, ONE run.** The ONE command performs **all six** ensures described below — orchestration config · gate declaration · autonomy declaration · enforcement scripts · spec layer · navigator index — in a fixed order, and prints **one outcome line per ensure**: paste those lines into the step 4 / step 8 success report. Every SEED is **create-only** (an existing file is preserved byte-for-byte); the three refresh-class ops are named apart — the orchestration onboarding note (refreshed only while it still matches a canonical this kit shipped), the spec-layer reader and checker pairs (below) and the **navigator index**, a GENERATED artifact regenerated whenever it is missing or stale (never authored content, so there is nothing to preserve). One ensure failing **never** skips the others: each reports its own outcome and the run exits non-zero when any of them `failed`. The outcome tokens, by ensure: orchestration → `seeded` / `note-refreshed` / `already-current` / `customized-preserved` / `malformed-preserved`; gates and autonomy → `seeded` / `already-present`; scripts → `seeded` / `already-present` / `old-adr-layout-migration-instructed` / `skipped-no-node`; specs → `seeded` / `refreshed` / `already-present` / `customized-preserved` / `skipped-no-node`; index → `regenerated` / `already-current`; and any ensure may report `failed`, whose line OPENS with the cause (relay it with that cause — never soften it into a skip; an op that copies file by file also states when it stopped partway). The cause vocabulary is CLOSED — one of `race-unresolved`, `template-unreadable`, `bundle-unreadable`, `adr-layout-unverifiable`, `wrong-node-kind`, `write-refused`, `unexpected-error`, `generator-unlaunchable`, `generator-failed`, `index-probe-failed`, `index-stale-after-write` — and every cause that can only arise AFTER the generator ran (`generator-failed`, and `index-probe-failed` / `index-stale-after-write` when they follow a reported regeneration) DISCLOSES in its own line that a write may already have landed. **A non-zero exit STOPs this upgrade** — report the failed line and stop there, before the equal-head exit, the migrations and the re-stamp. Add `--dry-run` to preview without writing a byte. Like the pointer slots + the footprint reconcile, all six reach an equal-head deployment **without a lineage-head bump or a migration file** (they are `.json` / `scripts/` / a generated artifact / a seeded store root, inherently outside the docs cap-validator).
|
|
34
34
|
|
|
35
35
|
**What the orchestration-config ensure does.** `docs/ai/orchestration.json` must exist **and its onboarding note must be current**: created from the canonical seed if missing; if it already exists, **every activity/slot the user set is preserved** and ONLY the `_README` note is refreshed, and only when the existing one still matches a known prior canonical — the tested `refreshIfCanonical` / `refreshReadme` in `tools/orchestration-config.mjs` is the source of truth for that decision (it normalizes CRLF/whitespace before comparing; a *customized* `_README` is preserved verbatim → `customized-preserved`; a *malformed* existing config is **preserved untouched + LOUD** → `malformed-preserved`, never clobbered and never silently skipped). The current note points at `/agent-workflow-kit set-recipe`. **Kit-owned:** in the **delegated** path memory only seeds/preserves the file (memory upgrade step 2) and this ensure applies the `_README` refresh; in the **fallback** path it does both. (Memory stays standalone.)
|
|
36
36
|
|
|
@@ -42,7 +42,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
42
42
|
|
|
43
43
|
**What the navigator ensure does.** `docs/ai/index.md` is the always-loaded navigator the entry point declares, and it is GENERATED — no template ships it, so a deployment that never ran the generator boots from a broken entry point (and, on a Node project, carries a pre-commit hook that fails its own index check). The ensure runs the bundled generator's finalizer and reports `regenerated` (it was missing or stale — it was written) or `already-current` (nothing written). It never skips a No-Node project: the generator runs from `${CLAUDE_SKILL_DIR}/references/scripts/` on the agent host, not from the project's `scripts/`. **Its position in the run-list is EARLY and therefore NOT authoritative** — `lens` (and, on the migrated path, steps 6–7) still change `docs/ai` afterwards — so the authoritative run is the LATE `--only index` rung documented at both exits; the early one is idempotent and costs at most an `already-current` line.
|
|
44
44
|
|
|
45
|
-
**What the spec-layer ensure does.** A deployment older than the feature-spec layer lacks `scripts/spec-schema.mjs` and `docs/ai/specs/index.md`, and its deployed `check-docs-size.mjs` predates the store collapse. In ONE order: the reader pair
|
|
45
|
+
**What the spec-layer ensure does.** A deployment older than the feature-spec layer lacks `scripts/spec-schema.mjs` and `docs/ai/specs/index.md`, and its deployed `check-docs-size.mjs` predates the store collapse. In ONE order: the reader pair, then the checker pair — each seeded when absent and REFRESHED only while a file's bytes are a body a release shipped (an append-only digest catalog → `refreshed`; an edited body is preserved verbatim and withholds the writes that depend on it, the checker lane waiting on a byte-current reader pair); the store root is seeded from the bundled template, date rendered, ONLY behind a checker pair current after the run — an older or edited checker renders the store row by row and reds the hook's `--check-index`, so behind a custom checker the store root is NOT seeded and the line names the remedy (copy the pair from `${CLAUDE_SKILL_DIR}/references/scripts/` by hand, re-run). One token by precedence: `seeded` > `refreshed` > `customized-preserved` (an edited pair, and this run wrote nothing) > `already-present`; `skipped-no-node` without a `package.json`; every line states what this run did, and a write that stops partway names what landed. The legacy-ADR instruct never withholds it.
|
|
46
46
|
|
|
47
47
|
**`gates-migration` — legacy gates.json migration (consented preview — D8).** An EXISTING declaration may still carry the retired review-ledger / fold-completeness checks. Run the preview (dry-run — writes NOTHING), show the user the exact plan, and only on an explicit yes re-run it with `--apply`: canonical legacy entries (matched by their documented single-invocation cmd forms) are REMOVED, the canonical `unit-tests` cmd gains the built-in lcov reporters, and the coverage-check gate is ADDED last — atomic and COMPLETE, so the migrated declaration satisfies `run-gates --final`. **The checker rides a PRODUCER or is not declared at all** (`${CLAUDE_SKILL_DIR}/references/modes/gates.md`): with no gate producing the lcov it reads, the migration does NOT add it, an already-declared one is reported INERT, the result is not called final-run-capable, and the preview prints the paste-ready suite cmd to declare by hand — nothing is ever removed for you. CUSTOMIZED entries are NEVER auto-touched: the preview names each with a paste-ready recovery, and the commit guard must NOT be installed until they are resolved. This is the ONLY gates.json writer at upgrade (the consented FILL preview runs at init).
|
|
48
48
|
|
|
@@ -310,13 +310,45 @@ const checkParts = (parsed, at, errors) => {
|
|
|
310
310
|
if (duplicate !== undefined) errors.push({ rule: 'parts', message: `part "${duplicate}" is listed twice` });
|
|
311
311
|
};
|
|
312
312
|
|
|
313
|
-
// The verdict
|
|
314
|
-
//
|
|
315
|
-
//
|
|
313
|
+
// The structure verdict (additive, slice 2a): the DETERMINISTIC extraction of what parsed, per
|
|
314
|
+
// section, targets VERBATIM as written (`./x.md` and `./x/index.md` stay distinct strings). The
|
|
315
|
+
// grammar is per line for scenarios/children/parts — a malformed line is simply absent — while the
|
|
316
|
+
// module is a conjunction (ONE `dir/` root or an all-file list): prose, a refused path, a mix or
|
|
317
|
+
// `*(empty)*` extracts null. `## Links` is free prose and is never extracted.
|
|
318
|
+
const scenarioEntry = (line) => {
|
|
319
|
+
const scenario = parseScenario(line);
|
|
320
|
+
if (scenario === null) return null;
|
|
321
|
+
return { ordinal: scenario.n, binding: scenario.bound ? { file: scenario.path, marker: scenario.marker } : null };
|
|
322
|
+
};
|
|
323
|
+
const linkEntry = (re) => (line) => {
|
|
324
|
+
const m = line.match(re);
|
|
325
|
+
return m && SLUG_RE.test(m[2]) ? { name: m[1], target: `./${m[2]}${m[3] ?? ''}.md` } : null;
|
|
326
|
+
};
|
|
327
|
+
const entriesOf = (parsed, heading, entryOf) =>
|
|
328
|
+
contentOf(sectionLines(parsed, heading) ?? []).map(entryOf).filter((entry) => entry !== null);
|
|
329
|
+
const extractModule = (parsed) => {
|
|
330
|
+
const content = contentOf(sectionLines(parsed, '## Module') ?? []);
|
|
331
|
+
const paths = bulletsOf(content);
|
|
332
|
+
if (paths.length === 0 || content.some((line) => !isBullet(line))) return null;
|
|
333
|
+
const kinds = paths.map(classifyPath);
|
|
334
|
+
if (paths.length === 1 && kinds[0] === 'dir') return { form: 'root', paths };
|
|
335
|
+
return kinds.every((kind) => kind === 'file') ? { form: 'fileSet', paths } : null;
|
|
336
|
+
};
|
|
337
|
+
const extractStructure = (parsed) => ({
|
|
338
|
+
scenarios: entriesOf(parsed, '## Scenarios', scenarioEntry),
|
|
339
|
+
children: entriesOf(parsed, '## Children', linkEntry(CHILD_LINK_RE)),
|
|
340
|
+
parts: entriesOf(parsed, '## Parts', linkEntry(PART_LINK_RE)),
|
|
341
|
+
module: extractModule(parsed),
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// The verdict: { kind, status, revision, structure, errors: [{ rule, message }], warnings: [{ rule,
|
|
345
|
+
// message }] }. Errors are collected past the first defect wherever later checks stay meaningful; a
|
|
346
|
+
// missing or defective frontmatter and an unknown kind each end the read (structure stays null),
|
|
347
|
+
// because no shape can be judged without them.
|
|
316
348
|
export const readSpecDocument = (text, rel) => {
|
|
317
349
|
const errors = [];
|
|
318
350
|
const warnings = [];
|
|
319
|
-
const verdict = (kind, status, revision) => ({ kind, status, revision, errors, warnings });
|
|
351
|
+
const verdict = (kind, status, revision, structure = null) => ({ kind, status, revision, structure, errors, warnings });
|
|
320
352
|
const front = parseFrontmatter(text.replace(/\r\n/g, '\n'));
|
|
321
353
|
if (front === null) {
|
|
322
354
|
errors.push({ rule: 'frontmatter', message: 'missing YAML frontmatter' });
|
|
@@ -351,5 +383,5 @@ export const readSpecDocument = (text, rel) => {
|
|
|
351
383
|
}
|
|
352
384
|
const status = kind === 'spec' ? fields.status ?? null : null;
|
|
353
385
|
const revision = kind === 'spec' && REVISION_RE.test(fields.revision ?? '') ? Number(fields.revision) : null;
|
|
354
|
-
return verdict(kind, status, revision);
|
|
386
|
+
return verdict(kind, status, revision, extractStructure(parsed));
|
|
355
387
|
};
|
|
@@ -194,6 +194,73 @@ describe('readSpecDocument — refuse, exactly one rule per defect', () => {
|
|
|
194
194
|
});
|
|
195
195
|
});
|
|
196
196
|
|
|
197
|
+
describe('readSpecDocument — the structure verdict (additive, slice 2a)', () => {
|
|
198
|
+
const structureOf = (text, rel) => readSpecDocument(text, rel).structure;
|
|
199
|
+
const BOTH_SCENARIOS = [
|
|
200
|
+
{ ordinal: 1, binding: { file: 'test/login.test.mjs', marker: 'spec:login/S1' } },
|
|
201
|
+
{ ordinal: 2, binding: null },
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
it('a flat spec extracts scenarios (bound + unbound) and its one dir/ module root', () => {
|
|
205
|
+
expect(structureOf(specDoc(), 'login.md')).toEqual({
|
|
206
|
+
scenarios: BOTH_SCENARIOS,
|
|
207
|
+
children: [],
|
|
208
|
+
parts: [],
|
|
209
|
+
module: { form: 'root', paths: ['src/login/'] },
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('a promoted root extracts parts and a fileSet module; ## Links stays free prose, never extracted', () => {
|
|
214
|
+
const text = specDoc({ module: '- src/a.mjs\n- src/b.mjs', extra: '\n## Parts\n\n- [sessions](./sessions.md)\n\n## Links\n\n- [[AD-112]]\n' });
|
|
215
|
+
expect(structureOf(text, 'auth/login/index.md')).toEqual({
|
|
216
|
+
scenarios: BOTH_SCENARIOS,
|
|
217
|
+
children: [],
|
|
218
|
+
parts: [{ name: 'sessions', target: './sessions.md' }],
|
|
219
|
+
module: { form: 'fileSet', paths: ['src/a.mjs', 'src/b.mjs'] },
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('an index extracts children with VERBATIM targets — ./x.md and ./x/index.md stay distinct strings', () => {
|
|
224
|
+
expect(structureOf(indexDoc(), 'auth/index.md')).toEqual({
|
|
225
|
+
scenarios: [],
|
|
226
|
+
children: [{ name: 'login', target: './login.md' }, { name: 'billing', target: './billing/index.md' }],
|
|
227
|
+
parts: [],
|
|
228
|
+
module: null,
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('a part extracts the empty structure; a retired *(empty)* module extracts null on a CLEAN document', () => {
|
|
233
|
+
expect(structureOf(partDoc(), 'auth/login/sessions.md')).toEqual({ scenarios: [], children: [], parts: [], module: null });
|
|
234
|
+
const retired = specDoc({ fields: { status: 'retired' }, module: '*(empty)*', scenarios: ['- S1 gone :: unbound'] });
|
|
235
|
+
expect(structureOf(retired, 'login.md').module).toBeNull();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('EVERY early refusal reads structure null: missing frontmatter, a frontmatter defect, an unknown or absent kind', () => {
|
|
239
|
+
expect(structureOf('# Spec: Login\n', 'login.md')).toBeNull();
|
|
240
|
+
expect(structureOf(specDoc({ fields: { priority: 'high' } }), 'login.md')).toBeNull();
|
|
241
|
+
expect(structureOf(specDoc({ fields: { kind: 'feature' } }), 'login.md')).toBeNull();
|
|
242
|
+
expect(structureOf(specDoc({ drop: ['kind'] }), 'login.md')).toBeNull();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('a grammar-malformed scenario/child/part line is simply ABSENT — valid lines before and after it extract', () => {
|
|
246
|
+
const s = structureOf(specDoc({ scenarios: ['- S1 a :: unbound', '- S2 broken', '- S3 c :: unbound'] }), 'login.md');
|
|
247
|
+
expect(s.scenarios).toEqual([{ ordinal: 1, binding: null }, { ordinal: 3, binding: null }]);
|
|
248
|
+
const c = structureOf(indexDoc({ children: ['- [a](./a.md)', '- broken', '- [b](./b/index.md)'] }), 'auth/index.md');
|
|
249
|
+
expect(c.children).toEqual([{ name: 'a', target: './a.md' }, { name: 'b', target: './b/index.md' }]);
|
|
250
|
+
const p = structureOf(specDoc({ extra: '\n## Parts\n\n- [a](./a.md)\n- broken\n- [b](./b.md)\n' }), 'auth/login/index.md');
|
|
251
|
+
expect(p.parts).toEqual([{ name: 'a', target: './a.md' }, { name: 'b', target: './b.md' }]);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('the module is a CONJUNCTION — prose, a refused path, a dir/file mix each extract null; a rule-refused scenario line still extracts verbatim', () => {
|
|
255
|
+
for (const module of ['the root is\n- src/login/', '- ../src/', '- src/login/\n- src/login/a.mjs']) {
|
|
256
|
+
expect(structureOf(specDoc({ module }), 'login.md').module).toBeNull();
|
|
257
|
+
}
|
|
258
|
+
const v = readSpecDocument(specDoc({ scenarios: ['- S1 a :: test/a.mjs :: spec:login/S2'] }), 'login.md');
|
|
259
|
+
expect(v.errors.map((e) => e.rule)).toEqual(['scenario-marker']);
|
|
260
|
+
expect(v.structure.scenarios).toEqual([{ ordinal: 1, binding: { file: 'test/a.mjs', marker: 'spec:login/S2' } }]);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
197
264
|
describe('classifyPath + the frozen constants', () => {
|
|
198
265
|
it('classifies the lexical path forms', () => {
|
|
199
266
|
expect(['src/', 'src/a.mjs', '../x', '/x', 'C:/x', 'a\\b', 'src/*.mjs'].map(classifyPath)).toEqual([
|
package/tools/ensure-configs.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// gates docs/ai/gates.json seed-if-missing (an existing declaration is authored content)
|
|
6
6
|
// autonomy docs/ai/autonomy.json seed-if-missing (same)
|
|
7
7
|
// scripts scripts/<ADR enforcement> seed-if-missing, ADR-layout detect FIRST
|
|
8
|
-
// specs scripts/<spec layer> reader
|
|
8
|
+
// specs scripts/<spec layer> reader + checker pairs seeded when absent, refreshed only on
|
|
9
9
|
// + docs/ai/specs/index.md a shipped prior body, store root seeded behind a current checker
|
|
10
10
|
// index docs/ai/index.md regenerate-if-missing-or-stale (a GENERATED artifact)
|
|
11
11
|
//
|
|
@@ -64,9 +64,10 @@ Usage:
|
|
|
64
64
|
|
|
65
65
|
Every SEED is CREATE-ONLY: an existing file is preserved byte-for-byte, never clobbered and never
|
|
66
66
|
refreshed in place. Three ops refresh instead: the orchestration onboarding note, only while it still
|
|
67
|
-
matches a canonical the kit shipped (your own wording is preserved verbatim); the spec-layer
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
matches a canonical the kit shipped (your own wording is preserved verbatim); the spec-layer reader
|
|
68
|
+
and checker pairs, only while a file's bytes are a body a release shipped (an edited file is
|
|
69
|
+
preserved, and the writes that depend on it — the spec store root included — are withheld behind
|
|
70
|
+
it); and the navigator index — a GENERATED artifact, regenerated
|
|
70
71
|
whenever it is missing or stale. The enforcement-script ensure detects an older ADR-store layout
|
|
71
72
|
FIRST and instructs the opt-in migration instead of seeding.
|
|
72
73
|
|
package/tools/ensure-specs.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// ensure-specs.mjs — the SIXTH upgrade ensure: the spec layer (memory 4.6.0, AD-112) delivered to an
|
|
2
2
|
// EXISTING deployment on an equal-head upgrade. Four deployed scripts and one store root, written in
|
|
3
3
|
// a FIXED order the measured coupling dictates:
|
|
4
|
-
// 1. the reader pair scripts/spec-schema.mjs + .test.mjs —
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
4
|
+
// 1. the reader pair scripts/spec-schema.mjs + .test.mjs — created when absent; since 2a
|
|
5
|
+
// also REFRESHED when the deployed bytes are a body a release shipped (script-priors.mjs), a
|
|
6
|
+
// custom body preserved verbatim (brief D1 shipped it create-only; the 4.6.x bodies joining the
|
|
7
|
+
// catalog is what keeps those deployments out of the stranded-custom cell);
|
|
8
|
+
// 2. the checker pair scripts/check-docs-size.mjs + .test.mjs — the same prior-refresh lane, and
|
|
9
|
+
// only behind a reader pair this run leaves byte-current;
|
|
9
10
|
// 3. the store root docs/ai/specs/index.md — seeded (placeholders rendered)
|
|
10
11
|
// only once both pairs are current.
|
|
11
12
|
// Why the order: the kit's bundled navigator generator collapses `specs/` into one row, while the
|
|
@@ -24,7 +25,7 @@
|
|
|
24
25
|
import { readFileSync, lstatSync } from 'node:fs';
|
|
25
26
|
import { join } from 'node:path';
|
|
26
27
|
import { writeContainedFileAtomic, writeProjectFileCreateOnly } from './atomic-write.mjs';
|
|
27
|
-
import {
|
|
28
|
+
import { classifyDeployedScript } from './script-priors.mjs';
|
|
28
29
|
import { composeFailure, composeOutcome, isNodeProject, probeSeedTarget, tmpNote } from './ensure-ops.mjs';
|
|
29
30
|
|
|
30
31
|
const OP = 'specs';
|
|
@@ -33,8 +34,10 @@ const BUNDLED_SCRIPTS = ['references', 'scripts'];
|
|
|
33
34
|
const STORE_ROOT_REL = 'docs/ai/specs/index.md';
|
|
34
35
|
const STORE_ROOT_TEMPLATE = ['references', 'templates', 'specs', 'index.md'];
|
|
35
36
|
const DATE_PLACEHOLDER = '{{DATE}}';
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
// Explicit pairs, deliberately NOT aliased to the catalog's PRIOR_FILES (which spans both): each
|
|
38
|
+
// file is surveyed exactly once. Exported so the suite pins pairs-union === catalog domain.
|
|
39
|
+
export const READER_PAIR = Object.freeze(['spec-schema.mjs', 'spec-schema.test.mjs']);
|
|
40
|
+
export const CHECKER_PAIR = Object.freeze(['check-docs-size.mjs', 'check-docs-size.test.mjs']);
|
|
38
41
|
|
|
39
42
|
const ok = (token, lines) => composeOutcome(OP, token, lines, false);
|
|
40
43
|
const loud = (cause, ...lines) => composeFailure(OP, cause, ...lines);
|
|
@@ -67,14 +70,20 @@ const surveyStoreRoot = (cwd, lstat) => {
|
|
|
67
70
|
|
|
68
71
|
// ── the decision: which writes the survey admits (pure over the survey) ───────────────────────────
|
|
69
72
|
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
// Reader seeds are create-only and always admitted. A REFRESH overwrites bytes, so it runs only
|
|
74
|
+
// inside a pair whose every file is proven refreshable (current | prior | absent) — one custom file
|
|
75
|
+
// withholds its whole pair; the checker lane (its seeds included) and the store additionally wait
|
|
76
|
+
// for a reader pair this run leaves byte-current (seeded + refreshed = current).
|
|
77
|
+
const PAIR_ELIGIBLE = new Set(['current', 'prior', 'absent']);
|
|
72
78
|
|
|
73
79
|
export const decideWrites = ({ reader, checker, store }) => {
|
|
74
|
-
const
|
|
75
|
-
const checkerEligible =
|
|
80
|
+
const readerEligible = reader.every((f) => PAIR_ELIGIBLE.has(f.state));
|
|
81
|
+
const checkerEligible = readerEligible && checker.every((f) => PAIR_ELIGIBLE.has(f.state));
|
|
76
82
|
const writes = [];
|
|
77
|
-
for (const f of reader)
|
|
83
|
+
for (const f of reader) {
|
|
84
|
+
if (f.state === 'absent') writes.push({ kind: 'seed', file: f });
|
|
85
|
+
if (readerEligible && f.state === 'prior') writes.push({ kind: 'refresh', file: f });
|
|
86
|
+
}
|
|
78
87
|
if (checkerEligible) {
|
|
79
88
|
for (const f of checker) {
|
|
80
89
|
if (f.state === 'absent') writes.push({ kind: 'seed', file: f });
|
|
@@ -14,8 +14,8 @@ export const ENSURE_OPS = Object.freeze(['orchestration', 'gates', 'autonomy', '
|
|
|
14
14
|
|
|
15
15
|
// Tokens that assert a WRITE happened. --dry-run may never emit one of these (the CLI's contract test
|
|
16
16
|
// walks this set), and each has exactly one `would-` counterpart below. `refreshed` is the spec-layer
|
|
17
|
-
// ensure's
|
|
18
|
-
// bundled one (a custom body is never refreshed).
|
|
17
|
+
// ensure's pair refresh (reader or checker) — a deployed script on a body a release shipped,
|
|
18
|
+
// rewritten to the bundled one (a custom body is never refreshed).
|
|
19
19
|
export const WRITE_TOKENS = Object.freeze(['seeded', 'note-refreshed', 'refreshed', 'regenerated']);
|
|
20
20
|
export const DRY_RUN_TOKENS = Object.freeze(['would-seed', 'would-refresh-note', 'would-refresh', 'would-regenerate']);
|
|
21
21
|
|
package/tools/script-priors.mjs
CHANGED
|
@@ -13,9 +13,12 @@
|
|
|
13
13
|
|
|
14
14
|
import { createHash } from 'node:crypto';
|
|
15
15
|
|
|
16
|
-
// The deployed scripts the spec-layer ensure may REFRESH
|
|
17
|
-
//
|
|
18
|
-
|
|
16
|
+
// The deployed scripts the spec-layer ensure may REFRESH — the FULL refreshable catalog: the checker
|
|
17
|
+
// pair that imports the reader, and (since 2a) the reader pair itself. Every other deployed script is
|
|
18
|
+
// outside the refresh lane by design.
|
|
19
|
+
export const PRIOR_FILES = Object.freeze([
|
|
20
|
+
'check-docs-size.mjs', 'check-docs-size.test.mjs', 'spec-schema.mjs', 'spec-schema.test.mjs',
|
|
21
|
+
]);
|
|
19
22
|
|
|
20
23
|
const prior = (file, firstShipped, lastShipped, digest) => Object.freeze({ file, firstShipped, lastShipped, digest });
|
|
21
24
|
|
|
@@ -26,6 +29,8 @@ export const SCRIPT_PRIORS = Object.freeze([
|
|
|
26
29
|
prior('check-docs-size.mjs', '4.4.0', '4.5.0', '7a5cd7f98571c3248d0378623172e9c60073b8d8761bce7a95c263f99bfb3a42'),
|
|
27
30
|
prior('check-docs-size.mjs', '4.5.1', '4.5.4', 'fef3555b14a5ade46071bac18bd6dfc87daec39dd63ce1f7965864c3e51558d9'),
|
|
28
31
|
prior('check-docs-size.test.mjs', '4.0.0', '4.5.4', '88fbb3d7f097d74771b7c5d9ad99fcd58b274ae33f391e1ff01f4b138b9236cd'),
|
|
32
|
+
prior('spec-schema.mjs', '4.6.0', '4.6.1', 'f8ee23d81e90fd4225ca4ece288cba41982c4430290bc6d033f5ca18d2d283f4'),
|
|
33
|
+
prior('spec-schema.test.mjs', '4.6.0', '4.6.1', 'a12d6d3f5d32c6dabdee7e15af7d2ab15a0ced37515d1844fe0951f60cddbc99'),
|
|
29
34
|
]);
|
|
30
35
|
|
|
31
36
|
export const digestOf = (bytes) => createHash('sha256').update(bytes).digest('hex');
|