@sabaiway/agent-workflow-memory 4.6.1 → 4.7.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 CHANGED
@@ -4,6 +4,33 @@ All notable changes to the memory substrate. Versions are this **package's** npm
4
4
  they are distinct from the **deployment-lineage** stamp written into a project's
5
5
  `docs/ai/.memory-version` (which tracks the shared `agent-workflow` lineage, head `3.0.0`).
6
6
 
7
+ ## 4.7.0 — the reader's verdict gains the additive `structure` extraction (AD-114)
8
+
9
+ The slice-2 checker (`spec-check`, next release) must read a document's structure through the SAME
10
+ module that defines "malformed" — a second parser of the frozen grammar would fork that definition.
11
+ `readSpecDocument` therefore now returns, beside the untouched errors and warnings, an additive
12
+ `structure` field in a shape frozen at plan time:
13
+
14
+ - **`{scenarios: [{ordinal, binding: {file, marker} | null}], children: [{name, target}], parts:
15
+ [{name, target}], module: {form: 'root' | 'fileSet', paths: [...]} | null}`** — every target
16
+ VERBATIM as written (`./x.md` and `./x/index.md` stay distinct strings).
17
+ - **Null ONLY on the early refusals** (missing frontmatter, a frontmatter-key defect, an unknown
18
+ kind); for a known kind with errors it is the DETERMINISTIC extraction of what parsed: a
19
+ grammar-malformed scenario/child/part line is simply absent (valid lines before and after it
20
+ extract), while a line that parses but breaks a rule still extracts verbatim beside its error.
21
+ - **The module is a conjunction** — ONE `dir/` root or an all-file list; prose, a refused path, a
22
+ dir/file mix or `*(empty)*` extracts `module: null`. `## Links` stays free prose, never
23
+ extracted.
24
+ - The 33 rule ids, every refusal and every existing verdict field stay behaviour-identical (the
25
+ engine corpus suite is green untouched; engine stays 3.3.0). The deep-equal suite pins the shape
26
+ per kind, both module forms, every early-refusal branch and the partial extractions; the new
27
+ tests were red-proofed against the pre-change reader.
28
+
29
+ MINOR: a shipped script gains a capability. One prose alignment rides along: the upgrade's
30
+ refresh-lane sentence now names a file of EITHER deployed pair (reader or checker) on a shipped
31
+ body as refreshed by the composition root's upgrade — this substrate still carries no catalog and
32
+ never overwrites a deployed script itself.
33
+
7
34
  ## 4.6.1 — the standalone upgrade delivers the spec layer behind a checker it can prove (AD-113)
8
35
 
9
36
  4.6.0 shipped the feature-spec layer for FRESH bootstraps; an EXISTING deployment at lineage head
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: agent-workflow-memory
3
3
  description: Deploy or upgrade a portable AI-agent memory substrate in any project — an entry-point `AGENTS.md` (+ `CLAUDE.md` alias) and a structured `docs/ai/` context store with cap/archive/index enforcement. Use when the user wants to bootstrap `docs/ai/`, set up the Memory Map and session protocols, install the docs-rotation pre-commit hook, or run `/agent-workflow-memory` / `/agent-workflow-memory upgrade`. Triggers on "set up the memory system", "deploy the AI memory here", "bootstrap docs/ai", "upgrade the memory substrate". This is the substrate only — the workflow methodology (plan→execute→review, queue, Cleanup) is owned elsewhere and injected into AGENTS.md by the family composition root.
4
4
  disable-model-invocation: true
5
5
  metadata:
6
- version: '4.6.1'
6
+ version: '4.7.0'
7
7
  ---
8
8
 
9
9
  # agent-workflow-memory
@@ -236,7 +236,7 @@ Fill strategy:
236
236
  written) **byte-equal** to this skill's bundled copies (compare with `cmp`): the deployed checker
237
237
  is what the pre-commit hook runs, and an older or edited one renders the store row by row and
238
238
  reds its own index check. Otherwise seed NO store root and report which file differs: a
239
- checker still on a body a release shipped is refreshed by the composition root's own upgrade
239
+ file of either pair still on a body a release shipped is refreshed by the composition root's own upgrade
240
240
  (it carries the catalog of shipped bodies), an edited one is the user's to refresh by hand — this
241
241
  substrate carries no such catalog and never overwrites a deployed script. **Same gate, also
242
242
  stamp-independent — ensure the NAVIGATOR:** `docs/ai/index.md` is a GENERATED artifact the entry
package/capability.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "agent-workflow-memory",
5
5
  "kind": "memory-substrate",
6
- "version": "4.6.1",
6
+ "version": "4.7.0",
7
7
  "provides": ["context"],
8
8
  "roles": {},
9
9
  "detect": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sabaiway/agent-workflow-memory",
3
- "version": "4.6.1",
3
+ "version": "4.7.0",
4
4
  "description": "Portable, cross-agent memory substrate for AI coding agents — an AGENTS.md entry point + docs/ai context with cap/archive/index enforcement, deployable standalone or as part of the agent-workflow family. The memory layer of the agent-workflow family.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -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: { kind, status, revision, errors: [{ rule, message }], warnings: [{ rule, message }] }.
314
- // Errors are collected past the first defect wherever later checks stay meaningful; a missing or
315
- // defective frontmatter and an unknown kind each end the read, because no shape can be judged without them.
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([