@sabaiway/agent-workflow-kit 11.2.0 → 12.0.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 +17 -0
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/agents/review-lens.md +1 -0
- package/references/scripts/spec-schema.mjs +54 -30
- package/references/scripts/spec-schema.test.mjs +65 -0
- package/tools/procedures.mjs +2 -1
- package/tools/script-priors.mjs +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ 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
|
+
## 12.0.0 — the reader mirror refuses an open-list clause, the outgoing reader becomes a prior, the consult line names the second case and the lens walks around every check (AD-133)
|
|
8
|
+
|
|
9
|
+
**MAJOR by the AD-117 class.** `references/scripts/spec-schema.mjs` and its test are the memory 8.0.0 bodies
|
|
10
|
+
byte-identically — `spec-check` relays the new `open-list` rule unchanged, so a consumer store valid under the old canon
|
|
11
|
+
can refuse under this kit; the remedy is the closing clause, never a fourth case. `tools/script-priors.mjs` and
|
|
12
|
+
`FROZEN_PRIORS` gain the outgoing reader pair as `5.0.0`..`7.2.1` with its fixture bodies under
|
|
13
|
+
`test/fixtures/script-priors/5.0.0/`, so a deployed 5.0.0–7.2.1 reader classifies `prior`, never `custom`.
|
|
14
|
+
|
|
15
|
+
**The consult clause and the lens bullet.** `tools/procedures.mjs` `CONSULT_LINE` — two source literals joined into
|
|
16
|
+
ONE rendered line, each under the 1000-byte cap — renders inside the agy `--focus` payload "If this finding is the
|
|
17
|
+
second case against the same check, reply with the replacement invariant, not an added case."; the codex consult-brief
|
|
18
|
+
and the lens re-dispatch forms say "with the same payload"; `tools/procedures.test.mjs` pins the payload order for both
|
|
19
|
+
activities. `references/agents/review-lens.md` gains the "How to review" bullet **When the artifact is a plan, walk
|
|
20
|
+
around every check** (the S12 arm in `cheap-agents-derived.test.mjs`; the placed copy takes the same line on refresh).
|
|
21
|
+
`tools/spec-coverage.test/cli.test.mjs` characterizes the S6 exit partition (`--help` / `-h` as the sole argument, an
|
|
22
|
+
unreadable store, a stray `--reason` beside `--check`, the debt-write failure as an exception).
|
|
23
|
+
|
|
7
24
|
## 11.2.0 — a field report is triaged through a rendered procedure over a checked record (AD-132)
|
|
8
25
|
|
|
9
26
|
Three consumer field reports were triaged ad hoc — verify each claim on HEAD, record the verdict,
|
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: '
|
|
6
|
+
version: '12.0.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": "
|
|
3
|
+
"version": "12.0.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",
|
|
@@ -29,6 +29,7 @@ How to review:
|
|
|
29
29
|
restatement of design intent back to the orchestrator.
|
|
30
30
|
- **Respect what is already decided.** The prompt will name findings already folded and decisions
|
|
31
31
|
already locked. Re-raising them is churn; checking whether the FIX is correct is real work.
|
|
32
|
+
- **When the artifact is a plan, walk around every check.** For every check bullet of each governing spec, name the invariant it proves and one state that walks around it; a named state is a finding with the replacement invariant, never an added case.
|
|
32
33
|
- **Accuracy over volume.** A wrong finding costs the orchestrator more than a missed one — it
|
|
33
34
|
spends a verification cycle and erodes trust in the whole list. If an angle turns up nothing, say
|
|
34
35
|
so in one line and move on. Never pad.
|
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// Pure text in, verdict out: readSpecDocument(text, rel) never touches the filesystem and imports
|
|
5
|
-
// nothing, so it seeds layout-free into any deployment. The navigator's collapse (check-docs-size.mjs)
|
|
6
|
-
// and the future spec-check read through THIS module — "malformed" has one definition, never two.
|
|
7
|
-
// `rel` is the path INSIDE docs/ai/specs/ (the store root navigator is `index.md`).
|
|
8
|
-
//
|
|
9
|
-
// SPEC_SCHEMA carries the frozen values; the engine canon (references/specs.md) is pinned against it.
|
|
10
|
-
// A refusal names exactly one rule id per defect, so a fixture corpus can be read rule by rule.
|
|
2
|
+
// Canon: the methodology engine's references/specs.md (frozen schema, refusals table).
|
|
11
3
|
|
|
12
4
|
export const SPEC_SCHEMA = Object.freeze({
|
|
13
5
|
storePrefix: 'docs/ai/specs/',
|
|
@@ -34,17 +26,25 @@ export const SPEC_SCHEMA = Object.freeze({
|
|
|
34
26
|
rootOwnedKeys: Object.freeze(['status', 'revision']),
|
|
35
27
|
rootOwnedSections: Object.freeze(['## Scenarios', '## Out of scope', '## Module', '## Parts']),
|
|
36
28
|
scenarioGrammar: '- S<N> <name> :: <repo-relative test path> :: spec:<slug>/S<N> | - S<N> <name> :: unbound',
|
|
29
|
+
openList: Object.freeze({
|
|
30
|
+
checkVerbs: Object.freeze(['refuse', 'refusal', 'REFUSE', 'REFUSAL']),
|
|
31
|
+
enumerationForms: Object.freeze([Object.freeze([',', ' or ']), Object.freeze([' · '])]),
|
|
32
|
+
closureTokens: Object.freeze([
|
|
33
|
+
'only if', 'only when', 'the only', 'everything else', 'anything else', 'nothing else',
|
|
34
|
+
'every other', 'any other', 'otherwise', 'exactly', 'one of', 'closed',
|
|
35
|
+
]),
|
|
36
|
+
emphaticToken: 'ONLY',
|
|
37
|
+
}),
|
|
37
38
|
rules: Object.freeze([
|
|
38
39
|
'frontmatter', 'frontmatter-key', 'substrate-key', 'type', 'kind', 'maxlines', 'status', 'revision',
|
|
39
40
|
'root-owns', 'slug', 'kind-path', 'root-uplink', 'title', 'section-missing', 'section-order',
|
|
40
|
-
'section-forbidden', 'fence', 'children-link', 'children-duplicate', 'fan-out', 'scenario-line',
|
|
41
|
+
'section-forbidden', 'fence', 'open-list', 'children-link', 'children-duplicate', 'fan-out', 'scenario-line',
|
|
41
42
|
'scenario-number', 'scenario-marker', 'scenario-path', 'scenarios-empty', 'out-of-scope', 'module-line', 'module-empty',
|
|
42
43
|
'module-traversal', 'module-absolute', 'module-backslash', 'module-glob', 'module-mix', 'parts',
|
|
43
44
|
]),
|
|
44
45
|
});
|
|
45
46
|
|
|
46
|
-
//
|
|
47
|
-
// is clean collapse into ONE navigator row linking `navPath`; a row with reader errors stays visible.
|
|
47
|
+
// Consumer contract: scripts/check-docs-size.mjs.
|
|
48
48
|
export const SPECS_COLLAPSE = Object.freeze({
|
|
49
49
|
prefix: SPEC_SCHEMA.storePrefix,
|
|
50
50
|
navPath: `${SPEC_SCHEMA.storePrefix}${SPEC_SCHEMA.navigatorFile}`,
|
|
@@ -72,8 +72,6 @@ const FORBIDDEN_SECTIONS = Object.freeze({
|
|
|
72
72
|
|
|
73
73
|
const KNOWN_KEYS = Object.freeze([...SPEC_SCHEMA.substrateKeys, 'kind', ...SPEC_SCHEMA.rootOwnedKeys]);
|
|
74
74
|
|
|
75
|
-
// The frontmatter is a closed key set: an unknown key, a repeated key or a line that is not
|
|
76
|
-
// `key: value` is a defect (`frontmatter-key`), never silently dropped or last-one-wins.
|
|
77
75
|
const parseFrontmatter = (text) => {
|
|
78
76
|
const match = text.match(FRONTMATTER_RE);
|
|
79
77
|
if (!match) return null;
|
|
@@ -91,10 +89,6 @@ const parseFrontmatter = (text) => {
|
|
|
91
89
|
|
|
92
90
|
const FENCE_RE = /^ {0,3}(`{3,}|~{3,})/;
|
|
93
91
|
|
|
94
|
-
// The body as { title, sections: [{ heading, lines }], preamble, fenced } — the title is the FIRST
|
|
95
|
-
// `# ` line, every `## ` line opens a section, the lines before the first section are the preamble.
|
|
96
|
-
// The reader parses NO markdown code: a fence line is recorded (the `fence` refusal) rather than
|
|
97
|
-
// modelled, so a spec carries no code sample and no line is ever ambiguous between code and structure.
|
|
98
92
|
const parseBody = (body) => {
|
|
99
93
|
const sections = [];
|
|
100
94
|
const preamble = [];
|
|
@@ -116,8 +110,6 @@ const parseBody = (body) => {
|
|
|
116
110
|
return { title: state.title, sections, preamble, fenced, sectionBeforeTitle: state.sectionBeforeTitle };
|
|
117
111
|
};
|
|
118
112
|
|
|
119
|
-
// Lexical classification of a repo-relative path field — the same vocabulary for module roots and
|
|
120
|
-
// scenario bindings. Realpath/symlink containment needs the filesystem and is the checker's duty.
|
|
121
113
|
export const classifyPath = (path) => {
|
|
122
114
|
if (path.trim() === '') return 'empty';
|
|
123
115
|
if (path.includes('\\')) return 'backslash';
|
|
@@ -139,8 +131,47 @@ const MODULE_PATH_RULES = Object.freeze({
|
|
|
139
131
|
const isBullet = (line) => line === '-' || line.startsWith('- ');
|
|
140
132
|
const bulletsOf = (lines) => lines.filter(isBullet).map((line) => line.slice(2));
|
|
141
133
|
const contentOf = (lines) => lines.filter((line) => line.trim() !== '');
|
|
134
|
+
const isAtxHeading = (line) => /^ {0,3}#{1,6}(?:[ \t]|$)/.test(line);
|
|
135
|
+
const topLevelBulletsOf = (lines) => lines.reduce((state, raw) => {
|
|
136
|
+
const line = raw.replace(/\s+$/, '');
|
|
137
|
+
if (isBullet(line)) {
|
|
138
|
+
state.current = [line === '-' ? '' : line.slice(2)];
|
|
139
|
+
state.bullets.push(state.current);
|
|
140
|
+
} else if (line.trim() === '' || isAtxHeading(line)) state.current = null;
|
|
141
|
+
else if (state.current !== null) state.current.push(line.trim());
|
|
142
|
+
return state;
|
|
143
|
+
}, { bullets: [], current: null }).bullets.map((parts) => parts.join(' ').replace(/\s+/g, ' ').trim());
|
|
144
|
+
|
|
145
|
+
const CLAUSE_BREAK = String.fromCharCode(0);
|
|
146
|
+
const MASKED_DOT = String.fromCharCode(1);
|
|
147
|
+
const MASKED_SEMICOLON = String.fromCharCode(2);
|
|
148
|
+
const maskClauseSpans = (text) => [...text].reduce((state, character) => {
|
|
149
|
+
if (character === '`') state.inSpan = !state.inSpan;
|
|
150
|
+
state.output.push(state.inSpan && character === '.' ? MASKED_DOT : state.inSpan && character === ';' ? MASKED_SEMICOLON : character);
|
|
151
|
+
return state;
|
|
152
|
+
}, { inSpan: false, output: [] }).output.join('');
|
|
153
|
+
const clausesOf = (bullet) => maskClauseSpans(bullet).replace(/([.;][*`\)\]]*) /g, `$1${CLAUSE_BREAK}`).split(CLAUSE_BREAK)
|
|
154
|
+
.map((clause) => clause.split(MASKED_DOT).join('.').split(MASKED_SEMICOLON).join(';').trim()).filter(Boolean);
|
|
155
|
+
const containsInOrder = (text, tokens) => tokens.reduce((position, token) => {
|
|
156
|
+
const found = position === -1 ? -1 : text.indexOf(token, position);
|
|
157
|
+
return found === -1 ? -1 : found + token.length;
|
|
158
|
+
}, 0) !== -1;
|
|
159
|
+
const hasBoundedToken = (clause, token, flags) => new RegExp(`(^|[^A-Za-z0-9_-])${token}([^A-Za-z0-9_-]|$)`, flags).test(clause);
|
|
160
|
+
const checkOpenLists = (parsed, kind, errors) => {
|
|
161
|
+
const groups = kind === 'spec' ? parsed.sections.filter((section) => section.heading === '## Contract').map((section) => section.lines)
|
|
162
|
+
: kind === 'part' ? [parsed.preamble, ...parsed.sections.map((section) => section.lines)] : [];
|
|
163
|
+
for (const clause of groups.flatMap(topLevelBulletsOf).flatMap(clausesOf)) {
|
|
164
|
+
const hasVerb = SPEC_SCHEMA.openList.checkVerbs.some((verb) => clause.includes(verb));
|
|
165
|
+
const hasEnumeration = SPEC_SCHEMA.openList.enumerationForms.some((form) => containsInOrder(clause, form));
|
|
166
|
+
const hasClosure = SPEC_SCHEMA.openList.closureTokens.some((token) => hasBoundedToken(clause, token, 'i'))
|
|
167
|
+
|| hasBoundedToken(clause, SPEC_SCHEMA.openList.emphaticToken, '');
|
|
168
|
+
if (hasVerb && hasEnumeration && !hasClosure) {
|
|
169
|
+
const prefix = clause.split(/\s+/).slice(0, 6).join(' ');
|
|
170
|
+
errors.push({ rule: 'open-list', message: `the clause "${prefix}…" is an open list — add a closing form to it such as "only if …; everything else …"` });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
142
174
|
|
|
143
|
-
// The slug a document owns: the file stem for a flat file, the folder name for an index.md.
|
|
144
175
|
const describeRel = (rel) => {
|
|
145
176
|
const segments = rel.split('/');
|
|
146
177
|
const file = segments[segments.length - 1];
|
|
@@ -314,11 +345,7 @@ const checkParts = (parsed, at, errors) => {
|
|
|
314
345
|
if (duplicate !== undefined) errors.push({ rule: 'parts', message: `part "${duplicate}" is listed twice` });
|
|
315
346
|
};
|
|
316
347
|
|
|
317
|
-
//
|
|
318
|
-
// section, targets VERBATIM as written (`./x.md` and `./x/index.md` stay distinct strings). The
|
|
319
|
-
// grammar is per line for scenarios/children/parts — a malformed line is simply absent — while the
|
|
320
|
-
// module is a conjunction (ONE `dir/` root or an all-file list): prose, a refused path, a mix or
|
|
321
|
-
// `*(empty)*` extracts null. `## Links` is free prose and is never extracted.
|
|
348
|
+
// Structure verdict: the same canon, "The frozen schema".
|
|
322
349
|
const scenarioEntry = (line) => {
|
|
323
350
|
const scenario = parseScenario(line);
|
|
324
351
|
if (scenario === null) return null;
|
|
@@ -345,10 +372,6 @@ const extractStructure = (parsed) => ({
|
|
|
345
372
|
module: extractModule(parsed),
|
|
346
373
|
});
|
|
347
374
|
|
|
348
|
-
// The verdict: { kind, status, revision, structure, errors: [{ rule, message }], warnings: [{ rule,
|
|
349
|
-
// message }] }. Errors are collected past the first defect wherever later checks stay meaningful; a
|
|
350
|
-
// missing or defective frontmatter and an unknown kind each end the read (structure stays null),
|
|
351
|
-
// because no shape can be judged without them.
|
|
352
375
|
export const readSpecDocument = (text, rel) => {
|
|
353
376
|
const errors = [];
|
|
354
377
|
const warnings = [];
|
|
@@ -372,6 +395,7 @@ export const readSpecDocument = (text, rel) => {
|
|
|
372
395
|
const at = checkPath(rel, kind, errors);
|
|
373
396
|
const parsed = parseBody(body);
|
|
374
397
|
if (parsed.fenced.length > 0) errors.push({ rule: 'fence', message: `"${parsed.fenced[0]}" — a spec document carries no code fence` });
|
|
398
|
+
checkOpenLists(parsed, kind, errors);
|
|
375
399
|
checkSections(parsed, kind, errors);
|
|
376
400
|
if (kind === 'index') {
|
|
377
401
|
checkChildren(parsed, errors);
|
|
@@ -32,6 +32,8 @@ const refuses = (text, rel, rule) => {
|
|
|
32
32
|
const verdict = readSpecDocument(text, rel);
|
|
33
33
|
expect(rulesOf(verdict)).toEqual([rule]);
|
|
34
34
|
};
|
|
35
|
+
const withContract = (contract) => specDoc().replace('Accepts a credential pair.', contract);
|
|
36
|
+
const openListErrors = (contract, rel = 'login.md') => readSpecDocument(withContract(contract), rel).errors;
|
|
35
37
|
|
|
36
38
|
describe('readSpecDocument — accept', () => {
|
|
37
39
|
it('a flat draft spec reads clean with kind/status/revision', () => {
|
|
@@ -269,6 +271,69 @@ describe('readSpecDocument — the structure verdict (additive, slice 2a)', () =
|
|
|
269
271
|
});
|
|
270
272
|
});
|
|
271
273
|
|
|
274
|
+
describe('readSpecDocument — open-list clauses', () => {
|
|
275
|
+
const refusal = 'the clause "an anchor path reported modified, untracked…" is an open list — add a closing form to it such as "only if …; everything else …"';
|
|
276
|
+
const open = '- an anchor path reported modified, untracked or ignored is the named refusal `anchor-dirty`';
|
|
277
|
+
const closed = '- an anchor path is accepted only if `ls-files -v` lists it with tag `H` and `status` is silent — everything else, a path reported modified, untracked or ignored, is the named refusal `anchor-dirty`';
|
|
278
|
+
|
|
279
|
+
it('refuses the measured clause with the exact message; verb alone, form alone and an in-clause closure pass', () => {
|
|
280
|
+
expect(openListErrors(open)).toEqual([{ rule: 'open-list', message: refusal }]);
|
|
281
|
+
for (const text of [closed, '- refuse one case', '- alpha, beta or gamma']) expect(openListErrors(text)).toEqual([]);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('joins continuation lines, collapses whitespace and keeps an indented bullet in its top-level bullet', () => {
|
|
285
|
+
for (const text of ['- refuse alpha,\n beta or ignored', '- refuse alpha,\n - beta or ignored']) expect(rulesOf(readSpecDocument(withContract(text), 'login.md'))).toEqual(['open-list']);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('ends a bullet at a blank, the next top-level bullet or every ATX heading; Contract prose is ignored', () => {
|
|
289
|
+
for (const tail of ['\n\nEverything else closes', '\n- Everything else closes', ...Array.from({ length: 6 }, (_, i) => `\n${'#'.repeat(i + 1)} Everything else closes`)]) {
|
|
290
|
+
expect(rulesOf(readSpecDocument(withContract('- refuse alpha, beta or gamma' + tail), 'login.md'))).toEqual(['open-list']);
|
|
291
|
+
}
|
|
292
|
+
expect(openListErrors('refuse alpha, beta or gamma')).toEqual([]);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('ends a bullet at indented and tab-separated ATX headings', () => {
|
|
296
|
+
for (const tail of ['\n ## Everything else closes', '\n#\tEverything else closes']) expect(rulesOf(readSpecDocument(withContract('- refuse alpha, beta or gamma' + tail), 'login.md'))).toEqual(['open-list']);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('starts a new empty top-level bullet before following prose', () => {
|
|
300
|
+
for (const tail of ['\n- \nEverything else closes', '\n-\nonly if closes']) expect(rulesOf(readSpecDocument(withContract('- refuse alpha, beta or gamma' + tail), 'login.md'))).toEqual(['open-list']);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('judges an open list in every Contract section', () => {
|
|
304
|
+
const duplicate = specDoc().replace('## Scenarios', '## Contract\n\n- refuse alpha, beta or gamma\n\n## Scenarios');
|
|
305
|
+
expect(rulesOf(readSpecDocument(duplicate, 'login.md')).includes('open-list')).toBe(true);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('splits clauses at punctuation plus markdown tails, but not inside backtick spans', () => {
|
|
309
|
+
for (const tail of ['.** ', '.` ', '.) ', '.] ', '; ']) expect(rulesOf(readSpecDocument(withContract(`- refuse alpha, beta or gamma${tail}Everything else closes`), 'login.md'))).toEqual(['open-list']);
|
|
310
|
+
for (const text of ['- refuse `alpha. beta`, gamma or delta', '- refuse `alpha; beta`, gamma or delta', '- refuse `alpha. beta, gamma or delta']) expect(rulesOf(readSpecDocument(withContract(text), 'login.md'))).toEqual(['open-list']);
|
|
311
|
+
expect(openListErrors('- refuse alpha``. beta, gamma or delta')).toEqual([]);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('keeps span content in judgement, and a closure in a neighbouring clause cannot lift a refusal', () => {
|
|
315
|
+
expect(rulesOf(readSpecDocument(withContract('- `refuse` alpha, beta or gamma'), 'login.md'))).toEqual(['open-list']);
|
|
316
|
+
expect(openListErrors('- refuse alpha, beta or gamma with `everything else`')).toEqual([]);
|
|
317
|
+
expect(rulesOf(readSpecDocument(withContract('- refuse alpha, beta or gamma. Everything else closes'), 'login.md'))).toEqual(['open-list']);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('matches verbs, forms and closure tokens with the frozen case and boundary rules', () => {
|
|
321
|
+
for (const verb of ['refuse', 'refuses', 'refused', 'refusal', 'refusals', 'REFUSE', 'REFUSAL']) expect(rulesOf(readSpecDocument(withContract(`- ${verb} alpha, beta or gamma`), 'login.md'))).toEqual(['open-list']);
|
|
322
|
+
for (const text of ['- Refuse alpha, beta or gamma', '- refuse alpha, beta and gamma']) expect(openListErrors(text)).toEqual([]);
|
|
323
|
+
expect(rulesOf(readSpecDocument(withContract('- refuse alpha · beta'), 'login.md'))).toEqual(['open-list']);
|
|
324
|
+
for (const token of SPEC_SCHEMA.openList.closureTokens) expect(openListErrors(`- ${token.toUpperCase()}: refuse alpha, beta or gamma`)).toEqual([]);
|
|
325
|
+
for (const text of ['- refuse alpha, beta or gamma — none of these', '- refuse alpha, beta or gamma — closed-key', '- refuse alpha, beta or gamma — READ-ONLY', '- refuse alpha, beta or gamma — if only this', '- refuse alpha, beta or gamma — not only this']) expect(rulesOf(readSpecDocument(withContract(text), 'login.md'))).toEqual(['open-list']);
|
|
326
|
+
expect(openListErrors('- refuse alpha, beta or gamma — ONLY this')).toEqual([]);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('judges every part bullet, never an index, and freezes every schema list and inner pair', () => {
|
|
330
|
+
expect(rulesOf(readSpecDocument(partDoc({ extra: `\n${open}\n` }), 'auth/login/sessions.md'))).toEqual(['open-list']);
|
|
331
|
+
expect(readSpecDocument(indexDoc({ children: [], preamble: `\n${open}\n` }), 'auth/index.md').errors).toEqual([]);
|
|
332
|
+
expect(SPEC_SCHEMA.openList).toEqual({ checkVerbs: ['refuse', 'refusal', 'REFUSE', 'REFUSAL'], enumerationForms: [[',', ' or '], [' · ']], closureTokens: ['only if', 'only when', 'the only', 'everything else', 'anything else', 'nothing else', 'every other', 'any other', 'otherwise', 'exactly', 'one of', 'closed'], emphaticToken: 'ONLY' });
|
|
333
|
+
expect([SPEC_SCHEMA.openList, SPEC_SCHEMA.openList.checkVerbs, SPEC_SCHEMA.openList.enumerationForms, ...SPEC_SCHEMA.openList.enumerationForms, SPEC_SCHEMA.openList.closureTokens].every(Object.isFrozen)).toBe(true);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
272
337
|
describe('classifyPath + the frozen constants', () => {
|
|
273
338
|
it('classifies the lexical path forms', () => {
|
|
274
339
|
expect(['src/', 'src/a.mjs', '../x', '/x', 'C:/x', 'a\\b', 'src/*.mjs'].map(classifyPath)).toEqual([
|
package/tools/procedures.mjs
CHANGED
|
@@ -232,7 +232,8 @@ const backendSetLabel = (backends) =>
|
|
|
232
232
|
// review slot can resolve or request reviewed|council (execute floors at solo|delegated), so the gate
|
|
233
233
|
// reads the recipe, the requested recipe and the roster.
|
|
234
234
|
const REVIEW_RECIPES = new Set(['reviewed', 'council']);
|
|
235
|
-
const CONSULT_LINE = ' • Before every fold of a finding raised by a review member (a bridge backend or a placed lens): ASK that member whether the proposed fold solves it and adds no new problem; WAIT for its answer, READ it, then edit only as accepted or corrected — agy: agy-review --continue --decided @f --focus "Finding: <finding>. Proposed fold: <exact fold>. Does this proposed fold solve the finding and add no new problem?
|
|
235
|
+
const CONSULT_LINE = ' • Before every fold of a finding raised by a review member (a bridge backend or a placed lens): ASK that member whether the proposed fold solves it and adds no new problem; WAIT for its answer, READ it, then edit only as accepted or corrected — agy: agy-review --continue --decided @f --focus "Finding: <finding>. Proposed fold: <exact fold>. Does this proposed fold solve the finding and add no new problem? If this finding is the second case against the same check, reply with the replacement invariant, not an added case. ' +
|
|
236
|
+
'Reply accept, or correct with exact replacement text."; codex: fresh codex-review plan <consult-brief> with the same payload written before the working tree changes; a placed lens: re-dispatch the same lens vehicle with the same payload. A self-review finding, or any finding when no review member ran, is folded directly — the exemption is the finding\'s ORIGIN, never the recipe word on the slot line.';
|
|
236
237
|
const ARMED_CONSULT_LINE = ' • ARMED pre-fold sequence for a bridge-raised finding: the round is open → dispatch that bridge\'s consult with a nonce → WAIT and READ → fold accepted or corrected → flow-writer consult-attestation <planId> --backend <id> --nonce <n> --proposed-fix-digest <the-sha256-of-the-fold-text> → then edit. A lens-raised finding re-dispatches the lens without a nonce, WAIT and READ, then edit as accepted or corrected — it mints no manifest and no attestation — only its per-round participation rides its internal-attestation.';
|
|
237
238
|
// activity-aware (AD-046): the triage classification vocabulary rides EVERY review-backed activity;
|
|
238
239
|
// the LEDGER pointer renders ONLY for plan-execution — the ledger is plan-execution-scoped (AD-045),
|
package/tools/script-priors.mjs
CHANGED
|
@@ -34,6 +34,8 @@ export const SCRIPT_PRIORS = Object.freeze([
|
|
|
34
34
|
prior('spec-schema.mjs', '4.7.0', '4.7.0', '40b5b038d5ec5ed53c327c6d269d22fe5fa2bed99ae711fbf84306ad047be452'),
|
|
35
35
|
prior('spec-schema.test.mjs', '4.7.0', '4.7.0', 'fde896419924223e54cfcabfdb1ef5807df5386fac700ed7c1463b6e7f81501b'),
|
|
36
36
|
prior('check-docs-size.mjs', '4.6.0', '6.0.0', '22d020c3668cdbfb4cbc1f67a8a85b2baa2d0c4956808a50626d37409e81ab38'),
|
|
37
|
+
prior('spec-schema.mjs', '5.0.0', '7.2.1', '6a7d1e836c158baab5a412a1ad76d34891934dc28c88486c264a4b5d46f5892a'),
|
|
38
|
+
prior('spec-schema.test.mjs', '5.0.0', '7.2.1', '698564d7283281be1826019d214df475bb876687915de086818d8f5ece0e0336'),
|
|
37
39
|
]);
|
|
38
40
|
|
|
39
41
|
export const digestOf = (bytes) => createHash('sha256').update(bytes).digest('hex');
|