@warpgogol/forge 2.8.0 → 2.8.2
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/AGENTS.md +1 -1
- package/README.md +23 -13
- package/README.uk.md +19 -12
- package/dist/os/session/handlers/validate.d.ts.map +1 -1
- package/dist/os/session/handlers/validate.js +23 -1
- package/dist/os/session/handlers/validate.js.map +1 -1
- package/dist/os/session/types.d.ts +31 -1
- package/dist/os/session/types.d.ts.map +1 -1
- package/dist/os/session/types.js +7 -0
- package/dist/os/session/types.js.map +1 -1
- package/os/adr/adr.module.ts +158 -0
- package/os/adr/frontmatter-io.ts +80 -0
- package/os/adr/handlers/archive.ts +244 -0
- package/os/adr/handlers/implement-stamp.ts +331 -0
- package/os/adr/handlers/list-create.ts +207 -0
- package/os/adr/handlers/validate.test.ts +232 -0
- package/os/adr/handlers/validate.ts +429 -0
- package/os/adr/index.ts +33 -0
- package/os/adr/types.ts +141 -0
- package/os/audit/audit.module.ts +48 -0
- package/os/audit/frontmatter-io.ts +84 -0
- package/os/audit/handlers/archive.ts +239 -0
- package/os/audit/index.ts +23 -0
- package/os/audit/types.ts +29 -0
- package/os/compass/compass.module.ts +186 -0
- package/os/compass/handlers/compass-audit-handler.ts +382 -0
- package/os/compass/handlers/compass-change-summary-handler.ts +272 -0
- package/os/compass/handlers/compass-inventory-handler.ts +294 -0
- package/os/compass/handlers/compass-inventory.ts +521 -0
- package/os/compass/handlers/git-revision.ts +137 -0
- package/os/compass/handlers/resolve-scan-root-workpiece.test.ts +65 -0
- package/os/compass/handlers/resolve-scan-root.ts +79 -0
- package/os/compass/index.ts +33 -0
- package/os/core/core.module.ts +948 -0
- package/os/core/handlers/assets-check.ts +122 -0
- package/os/core/handlers/assets-helpers.ts +174 -0
- package/os/core/handlers/assets-list.ts +99 -0
- package/os/core/handlers/build.ts +155 -0
- package/os/core/handlers/determinism-check.ts +354 -0
- package/os/core/handlers/dev.ts +127 -0
- package/os/core/handlers/invariant-engine.test.ts +681 -0
- package/os/core/handlers/knowledge-compact.ts +248 -0
- package/os/core/handlers/lifecycle-handlers.test.ts +524 -0
- package/os/core/handlers/note-frontmatter-validate.test.ts +93 -0
- package/os/core/handlers/note-link-validate.test.ts +117 -0
- package/os/core/handlers/note-orphan-detect.test.ts +94 -0
- package/os/core/handlers/package-health.test.ts +226 -0
- package/os/core/handlers/package-health.ts +231 -0
- package/os/core/handlers/pinned-check.ts +200 -0
- package/os/core/handlers/pinned-init.ts +333 -0
- package/os/core/handlers/pinned-types.ts +50 -0
- package/os/core/handlers/pinned-validate.ts +301 -0
- package/os/core/handlers/profile-resolve.ts +92 -0
- package/os/core/handlers/release-prepare.ts +294 -0
- package/os/core/handlers/release-publish.ts +211 -0
- package/os/core/handlers/validate.ts +262 -0
- package/os/core/handlers/workspace-deps.ts +55 -0
- package/os/core/index.ts +13 -0
- package/os/exploration/exploration.module.ts +90 -0
- package/os/exploration/frontmatter-io.ts +77 -0
- package/os/exploration/handlers/archive.ts +159 -0
- package/os/exploration/handlers/list.ts +77 -0
- package/os/exploration/handlers/show.ts +107 -0
- package/os/exploration/index.ts +25 -0
- package/os/exploration/types.ts +65 -0
- package/os/mission/handlers/archive.test.ts +391 -0
- package/os/mission/handlers/archive.ts +453 -0
- package/os/mission/index.ts +1 -0
- package/os/mission/mission.module.ts +48 -0
- package/os/mission/types.ts +41 -0
- package/os/naming/index.ts +14 -0
- package/os/naming/naming-convention.test.ts +163 -0
- package/os/naming/naming-convention.ts +378 -0
- package/os/naming/naming.module.ts +36 -0
- package/os/notes/index.ts +10 -0
- package/os/notes/notes.module.ts +109 -0
- package/os/plan/frontmatter-io.ts +82 -0
- package/os/plan/handlers/archive.ts +238 -0
- package/os/plan/index.ts +23 -0
- package/os/plan/plan.module.ts +48 -0
- package/os/plan/types.ts +29 -0
- package/os/program/discovery.ts +294 -0
- package/os/program/handlers/complete.ts +419 -0
- package/os/program/handlers/lease.ts +433 -0
- package/os/program/handlers/seal.ts +332 -0
- package/os/program/handlers/validate.ts +213 -0
- package/os/program/lease.ts +156 -0
- package/os/program/program.module.ts +212 -0
- package/os/program/schemas.ts +218 -0
- package/os/program/state.ts +264 -0
- package/os/rfc/acceptance.ts +344 -0
- package/os/rfc/decision-log.ts +310 -0
- package/os/rfc/dna-trace.ts +280 -0
- package/os/rfc/frontmatter-io.test.ts +132 -0
- package/os/rfc/frontmatter-io.ts +130 -0
- package/os/rfc/handlers/archive.ts +300 -0
- package/os/rfc/handlers/check.ts +238 -0
- package/os/rfc/handlers/implement-stamp.ts +515 -0
- package/os/rfc/handlers/index-graph.ts +237 -0
- package/os/rfc/handlers/lifecycle.test.ts +127 -0
- package/os/rfc/handlers/lifecycle.ts +249 -0
- package/os/rfc/handlers/list-create.ts +296 -0
- package/os/rfc/handlers/pipeline-status.ts +173 -0
- package/os/rfc/handlers/shared.ts +121 -0
- package/os/rfc/handlers/supersede-propose.ts +241 -0
- package/os/rfc/handlers/validate-rules.test.ts +891 -0
- package/os/rfc/handlers/validate-rules.ts +984 -0
- package/os/rfc/handlers/validate.ts +166 -0
- package/os/rfc/handlers.ts +19 -0
- package/os/rfc/index.ts +97 -0
- package/os/rfc/rfc.module.ts +416 -0
- package/os/rfc/types.ts +596 -0
- package/os/rfc/verification-evidence.ts +244 -0
- package/os/session/atif-parser.ts +166 -0
- package/os/session/frontmatter-io.ts +163 -0
- package/os/session/handlers/archive.ts +288 -0
- package/os/session/handlers/list.ts +174 -0
- package/os/session/handlers/save.ts +348 -0
- package/os/session/handlers/validate.ts +250 -0
- package/os/session/index.ts +50 -0
- package/os/session/session.module.ts +126 -0
- package/os/session/types.ts +203 -0
- package/os/spec/live-spec-list-show-validate.test.ts +257 -0
- package/os/spec/live-spec-list.ts +78 -0
- package/os/spec/live-spec-merge.test.ts +261 -0
- package/os/spec/live-spec-merge.ts +413 -0
- package/os/spec/live-spec-show.ts +117 -0
- package/os/spec/live-spec-types.ts +93 -0
- package/os/spec/live-spec-validate.ts +170 -0
- package/os/spec/spec-materialize.ts +390 -0
- package/os/spec/spec-schema.ts +174 -0
- package/os/spec/spec-status.ts +284 -0
- package/os/spec/spec-validate.ts +513 -0
- package/os/spec/spec.module.ts +135 -0
- package/os/werkstatt/handlers/lock.ts +168 -0
- package/os/werkstatt/handlers/schema.ts +47 -0
- package/os/werkstatt/handlers/werkstatt-lock-recover.ts +175 -0
- package/os/werkstatt/handlers/werkstatt-lock-status.ts +73 -0
- package/os/werkstatt/handlers/werkstatt-operation-validate.ts +104 -0
- package/os/werkstatt/index.ts +39 -0
- package/os/werkstatt/werkstatt.module.ts +62 -0
- package/os/workflow/handlers.ts +304 -0
- package/os/workflow/index.ts +28 -0
- package/os/workflow/types.ts +99 -0
- package/os/workflow/workflow.module.ts +54 -0
- package/package.json +3 -3
- package/profiles/godot-csharp.yaml +4 -4
- package/profiles/knowledge-typescript-turborepo.yaml +110 -0
- package/profiles/phaser-turborepo.yaml +5 -5
- package/skills/_shared/fo-session-summary.md +132 -16
- package/skills/fo/fo-add-tests/pbt-guide.md +13 -13
- package/skills/fo/fo-doc-audit/SKILL.md +6 -6
- package/skills/fo/fo-handoff/SKILL.md +5 -0
- package/skills/fo/fo-idea/SKILL.md +1 -1
- package/skills/fo/fo-review/SKILL.md +1 -1
- package/skills/fo/fo-session-retro/SKILL.md +8 -0
- package/skills/fo/fo-session-save/SKILL.md +8 -0
- package/src/cli-output.ts +59 -0
- package/src/config/__tests__/resolve-terminology.test.ts +45 -0
- package/src/config/forge-config.ts +524 -0
- package/src/forge-module.ts +39 -0
- package/src/index.ts +172 -0
- package/src/knowledge/__tests__/promote.test.ts +219 -0
- package/src/knowledge/__tests__/serialize.test.ts +192 -0
- package/src/knowledge/budgets.ts +202 -0
- package/src/knowledge/compact.ts +405 -0
- package/src/knowledge/index.ts +53 -0
- package/src/knowledge/parse.ts +227 -0
- package/src/knowledge/promote.ts +156 -0
- package/src/knowledge/schema.ts +112 -0
- package/src/knowledge/serialize.ts +72 -0
- package/src/migration-adapters/__tests__/ignored-files.test.ts +51 -0
- package/src/migration-adapters/__tests__/registry.test.ts +131 -0
- package/src/migration-adapters/__tests__/types.test.ts +86 -0
- package/src/migration-adapters/git-utils.ts +71 -0
- package/src/migration-adapters/ignored-files.ts +221 -0
- package/src/migration-adapters/index.ts +21 -0
- package/src/migration-adapters/node-typescript-pnpm/index.ts +189 -0
- package/src/migration-adapters/phaser-pnpm/index.ts +189 -0
- package/src/migration-adapters/registry.ts +61 -0
- package/src/migration-adapters/types.ts +68 -0
- package/src/onboarding/__tests__/workspace-discovery.test.ts +134 -0
- package/src/onboarding/agents-generate.ts +443 -0
- package/src/onboarding/create.ts +427 -0
- package/src/onboarding/doctor.ts +1297 -0
- package/src/onboarding/init.ts +337 -0
- package/src/onboarding/invariant-engine.ts +448 -0
- package/src/onboarding/memory-scaffold.ts +189 -0
- package/src/onboarding/nested-agents-generate.ts +93 -0
- package/src/onboarding/nested-agents-templates.ts +233 -0
- package/src/onboarding/profile-validate.ts +139 -0
- package/src/onboarding/scaffold-project.ts +299 -0
- package/src/onboarding/scaffold.ts +143 -0
- package/src/onboarding/upgrade.ts +558 -0
- package/src/onboarding/workspace-discovery.ts +167 -0
- package/src/profiles/__tests__/profile-schema.test.ts +228 -0
- package/src/profiles/__tests__/stack-profile.test.ts +190 -0
- package/src/profiles/__tests__/terminology-utils.test.ts +71 -0
- package/src/profiles/profile-schema.ts +350 -0
- package/src/profiles/stack-profile.ts +188 -0
- package/src/profiles/terminology-utils.ts +36 -0
- package/src/registry.ts +185 -0
- package/src/skill-schema.ts +37 -0
- package/src/tests/acceptance-criteria.test.ts +99 -0
- package/src/tests/adr-implement-stamp.test.ts +368 -0
- package/src/tests/agents-generate-domain.test.ts +317 -0
- package/src/tests/agents-generate.test.ts +458 -0
- package/src/tests/bindings-schema.test.ts +314 -0
- package/src/tests/budgets.test.ts +213 -0
- package/src/tests/cli-output.test.ts +196 -0
- package/src/tests/compact.test.ts +796 -0
- package/src/tests/create.test.ts +289 -0
- package/src/tests/doctor-autonomy.test.ts +98 -0
- package/src/tests/doctor-bindings.test.ts +286 -0
- package/src/tests/doctor-domain.test.ts +113 -0
- package/src/tests/exploration-handlers.test.ts +256 -0
- package/src/tests/fixtures/agents-generate-business-before.txt +288 -0
- package/src/tests/forge-config.test.ts +420 -0
- package/src/tests/fs-atomic.test.ts +96 -0
- package/src/tests/fs.test.ts +103 -0
- package/src/tests/generated-marker.test.ts +161 -0
- package/src/tests/hash.test.ts +37 -0
- package/src/tests/implement-stamp.test.ts +505 -0
- package/src/tests/init-bindings.test.ts +183 -0
- package/src/tests/knowledge-parse.test.ts +307 -0
- package/src/tests/knowledge-pbt.test.ts +164 -0
- package/src/tests/memory-scaffold.test.ts +192 -0
- package/src/tests/migration-adapters.test.ts +480 -0
- package/src/tests/package-files.test.ts +54 -0
- package/src/tests/pinned-check.test.ts +194 -0
- package/src/tests/pinned-init.test.ts +126 -0
- package/src/tests/profile-schema.test.ts +194 -0
- package/src/tests/profile-validate.test.ts +120 -0
- package/src/tests/program-lease.test.ts +245 -0
- package/src/tests/program-paths-extended.test.ts +117 -0
- package/src/tests/program-paths.test.ts +98 -0
- package/src/tests/program-property.test.ts +139 -0
- package/src/tests/program-schemas.test.ts +445 -0
- package/src/tests/program-spec-node.test.ts +250 -0
- package/src/tests/promote.test.ts +481 -0
- package/src/tests/registry.test.ts +73 -0
- package/src/tests/scaffold-project.test.ts +150 -0
- package/src/tests/session-handlers.test.ts +503 -0
- package/src/tests/session-pbt.test.ts +190 -0
- package/src/tests/skill-schema.test.ts +146 -0
- package/src/tests/skill-validate-knowledge.test.ts +26 -0
- package/src/tests/skill-validate.test.ts +254 -0
- package/src/tests/stack-profile.test.ts +221 -0
- package/src/tests/string-utils.test.ts +42 -0
- package/src/tests/upgrade.test.ts +428 -0
- package/src/tests/werkstatt-lock.test.ts +343 -0
- package/src/tests/workspace-discovery-domain.test.ts +112 -0
- package/src/tests/workspace-discovery.test.ts +135 -0
- package/src/types.ts +221 -0
- package/src/utils/__tests__/fs-idempotent.test.ts +73 -0
- package/src/utils/__tests__/fs-trash.test.ts +45 -0
- package/src/utils/fs-atomic.ts +93 -0
- package/src/utils/fs-idempotent.ts +41 -0
- package/src/utils/fs-trash-sync.ts +37 -0
- package/src/utils/fs-trash.ts +24 -0
- package/src/utils/fs.ts +74 -0
- package/src/utils/generated-marker.ts +166 -0
- package/src/utils/hash.ts +19 -0
- package/src/utils/index.ts +29 -0
- package/src/utils/string-utils.ts +19 -0
- package/src/validators/__tests__/note-orphan-detect.test.ts +94 -0
- package/src/validators/note-frontmatter-validate.ts +142 -0
- package/src/validators/note-link-validate.ts +145 -0
- package/src/validators/note-orphan-detect.ts +146 -0
- package/src/validators/port-validate.ts +97 -0
- package/src/validators/skill-validate.ts +831 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Register the session documentation domain commands with the forge kernel registry.</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not implement handler logic here — delegate to handlers/*.ts.</item>
|
|
6
|
+
</non-goals>
|
|
7
|
+
</MODULE_CONTRACT>
|
|
8
|
+
<CHANGE_SUMMARY>
|
|
9
|
+
<item>RFC-0537: initial forgeSessionModule registering session.save, session.archive, session.validate, session.list commands.</item>
|
|
10
|
+
</CHANGE_SUMMARY>
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ForgeModule } from "../../src/forge-module.ts";
|
|
14
|
+
|
|
15
|
+
export const forgeSessionModule: ForgeModule = {
|
|
16
|
+
name: "forge-session",
|
|
17
|
+
version: "0.1.0",
|
|
18
|
+
async register(registry) {
|
|
19
|
+
const { runSessionSave } = await import("./handlers/save.ts");
|
|
20
|
+
const { runSessionArchive } = await import("./handlers/archive.ts");
|
|
21
|
+
const { runSessionValidate } = await import("./handlers/validate.ts");
|
|
22
|
+
const { runSessionList } = await import("./handlers/list.ts");
|
|
23
|
+
|
|
24
|
+
registry.registerCommand({
|
|
25
|
+
name: "session.save",
|
|
26
|
+
description:
|
|
27
|
+
"Convert raw ATIF files from docs/sessions/.raw/ to structured markdown " +
|
|
28
|
+
"in docs/sessions/ with auto-extracted metadata. Idempotent — same raw " +
|
|
29
|
+
"file always produces the same output filename. Deletes raw file after " +
|
|
30
|
+
"conversion unless --keep-raw. Use --raw-file to process a specific file.",
|
|
31
|
+
scope: "workspace",
|
|
32
|
+
mutatesState: true,
|
|
33
|
+
writes: ["docs/sessions/*.md"],
|
|
34
|
+
reads: ["docs/sessions/.raw/*"],
|
|
35
|
+
cacheable: false,
|
|
36
|
+
flags: {
|
|
37
|
+
"raw-file": {
|
|
38
|
+
kind: "string",
|
|
39
|
+
description: "Process a specific raw file instead of scanning .raw/.",
|
|
40
|
+
},
|
|
41
|
+
"keep-raw": {
|
|
42
|
+
kind: "boolean",
|
|
43
|
+
description: "Do not delete raw file after conversion.",
|
|
44
|
+
},
|
|
45
|
+
"dry-run": {
|
|
46
|
+
kind: "boolean",
|
|
47
|
+
description: "Preview without writing files.",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
execute: runSessionSave,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
registry.registerCommand({
|
|
54
|
+
name: "session.archive",
|
|
55
|
+
description:
|
|
56
|
+
"Move session files older than --max-age-days (default 7) from " +
|
|
57
|
+
"docs/sessions/ to docs/sessions/archive/. Bidirectional: files in " +
|
|
58
|
+
"archive/ younger than threshold are moved back to docs/sessions/. " +
|
|
59
|
+
"Use --dry-run to preview. Age is computed from frontmatter date, " +
|
|
60
|
+
"not filesystem mtime. Prefer the docs.archive umbrella command " +
|
|
61
|
+
"unless you need to archive only sessions.",
|
|
62
|
+
scope: "workspace",
|
|
63
|
+
mutatesState: true,
|
|
64
|
+
writes: ["docs/sessions/*.md", "docs/sessions/archive/**"],
|
|
65
|
+
reads: ["docs/sessions/**/*.md"],
|
|
66
|
+
cacheable: false,
|
|
67
|
+
flags: {
|
|
68
|
+
"max-age-days": {
|
|
69
|
+
kind: "string",
|
|
70
|
+
description: "Age threshold in days (default 7).",
|
|
71
|
+
},
|
|
72
|
+
"dry-run": {
|
|
73
|
+
kind: "boolean",
|
|
74
|
+
description: "Preview what would be moved without touching the filesystem.",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
execute: runSessionArchive,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
registry.registerCommand({
|
|
81
|
+
name: "session.validate",
|
|
82
|
+
description:
|
|
83
|
+
"Validate session frontmatter schema (SES-01), id-filename match (SES-02), " +
|
|
84
|
+
"RFC-id existence (SES-03), raw file hygiene (SES-04), and non-markdown " +
|
|
85
|
+
"file detection (SES-05). Pass --id to validate a single file, " +
|
|
86
|
+
"or run without arguments for all. On-demand only — not integrated into " +
|
|
87
|
+
"build.check.",
|
|
88
|
+
scope: "workspace",
|
|
89
|
+
flags: {
|
|
90
|
+
id: { kind: "string", description: "Target a single session by id." },
|
|
91
|
+
},
|
|
92
|
+
reads: ["docs/sessions/**/*.md", "docs/rfcs/**/*.md"],
|
|
93
|
+
execute: runSessionValidate,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
registry.registerCommand({
|
|
97
|
+
name: "session.list",
|
|
98
|
+
description:
|
|
99
|
+
"List all sessions. Filter with --date-from, --date-to, --rfc, --type flags. " +
|
|
100
|
+
"Use --json for machine-readable output. Parses frontmatter on the fly. " +
|
|
101
|
+
"Includes archived sessions (marked with archived: true).",
|
|
102
|
+
scope: "workspace",
|
|
103
|
+
flags: {
|
|
104
|
+
"date-from": {
|
|
105
|
+
kind: "string",
|
|
106
|
+
description: "Filter sessions from this date (YYYY-MM-DD, inclusive).",
|
|
107
|
+
},
|
|
108
|
+
"date-to": {
|
|
109
|
+
kind: "string",
|
|
110
|
+
description: "Filter sessions up to this date (YYYY-MM-DD, inclusive).",
|
|
111
|
+
},
|
|
112
|
+
rfc: {
|
|
113
|
+
kind: "string",
|
|
114
|
+
description: "Filter by related RFC id (e.g. RFC-0537).",
|
|
115
|
+
},
|
|
116
|
+
type: {
|
|
117
|
+
kind: "string",
|
|
118
|
+
description:
|
|
119
|
+
"Filter by session type (mission, grilling, implementation, review, fix, freeform).",
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
reads: ["docs/sessions/**/*.md"],
|
|
123
|
+
execute: runSessionList,
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>
|
|
4
|
+
Type definitions and constants for the session documentation domain —
|
|
5
|
+
session frontmatter, list/archive/validate/save result shapes, and SES
|
|
6
|
+
validation rule identifiers.
|
|
7
|
+
</purpose>
|
|
8
|
+
<non-goals>
|
|
9
|
+
<item>Do not define handler logic here — that lives in handlers/*.ts.</item>
|
|
10
|
+
</non-goals>
|
|
11
|
+
</MODULE_CONTRACT>
|
|
12
|
+
<CHANGE_SUMMARY>
|
|
13
|
+
<item>RFC-0537: initial session domain types, constants, and SES rule identifiers.</item>
|
|
14
|
+
<item>RFC-0884: add checkpoint interfaces, extend SessionFrontmatter with optional checkpoint fields, add SES-06 rule.</item>
|
|
15
|
+
</CHANGE_SUMMARY>
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export const SESSION_DIR = "docs/sessions";
|
|
19
|
+
export const SESSION_RAW_SUBDIR = ".raw";
|
|
20
|
+
export const SESSION_ARCHIVE_SUBDIR = "archive";
|
|
21
|
+
export const SESSION_ID_PATTERN = /^\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}-[0-9a-f]{6}$/;
|
|
22
|
+
export const SESSION_FILE_PATTERN = /^\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}-[0-9a-f]{6}\.md$/;
|
|
23
|
+
|
|
24
|
+
export const SESSION_TYPES = [
|
|
25
|
+
"mission",
|
|
26
|
+
"grilling",
|
|
27
|
+
"implementation",
|
|
28
|
+
"review",
|
|
29
|
+
"fix",
|
|
30
|
+
"freeform",
|
|
31
|
+
] as const;
|
|
32
|
+
|
|
33
|
+
export type SessionType = (typeof SESSION_TYPES)[number];
|
|
34
|
+
|
|
35
|
+
export const SESSION_KNOWN_KEYS = [
|
|
36
|
+
"id",
|
|
37
|
+
"date",
|
|
38
|
+
"duration",
|
|
39
|
+
"types",
|
|
40
|
+
"summary",
|
|
41
|
+
"relatedRfcs",
|
|
42
|
+
"relatedArtifacts",
|
|
43
|
+
"decisions",
|
|
44
|
+
"commits",
|
|
45
|
+
"files",
|
|
46
|
+
"commands",
|
|
47
|
+
"systemDelta",
|
|
48
|
+
"diagrams",
|
|
49
|
+
"evidence",
|
|
50
|
+
"remainingIssues",
|
|
51
|
+
"checkpoint",
|
|
52
|
+
] as const;
|
|
53
|
+
|
|
54
|
+
export const SESSION_REQUIRED_KEYS = ["id", "date", "types"] as const;
|
|
55
|
+
|
|
56
|
+
export const SESSION_REQUIRED_SECTIONS = ["## Transcript"] as const;
|
|
57
|
+
|
|
58
|
+
// SES validation rules
|
|
59
|
+
export const SES_RULES = {
|
|
60
|
+
SES_01: "SES-01",
|
|
61
|
+
SES_02: "SES-02",
|
|
62
|
+
SES_03: "SES-03",
|
|
63
|
+
SES_04: "SES-04",
|
|
64
|
+
SES_05: "SES-05",
|
|
65
|
+
SES_06: "SES-06",
|
|
66
|
+
} as const;
|
|
67
|
+
|
|
68
|
+
export type SesRule = (typeof SES_RULES)[keyof typeof SES_RULES];
|
|
69
|
+
|
|
70
|
+
// ── Session frontmatter (the .md file's YAML frontmatter) ──
|
|
71
|
+
|
|
72
|
+
export interface SessionCheckpoint {
|
|
73
|
+
before: string;
|
|
74
|
+
change: string;
|
|
75
|
+
after: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface SessionDiagram {
|
|
79
|
+
type: "flowchart" | "sequenceDiagram" | "stateDiagram-v2" | "erDiagram" | "none";
|
|
80
|
+
scope: "current-system" | "session-delta";
|
|
81
|
+
caption: string;
|
|
82
|
+
mermaid: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface SessionEvidenceEntry {
|
|
86
|
+
claim: string;
|
|
87
|
+
source?: string;
|
|
88
|
+
test?: string;
|
|
89
|
+
command?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface SessionSystemDelta {
|
|
93
|
+
changedContracts: string[];
|
|
94
|
+
changedSchemas: string[];
|
|
95
|
+
changedStateMachines: string[];
|
|
96
|
+
changedInvariants: string[];
|
|
97
|
+
changedPersistence: string[];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface SessionFrontmatter {
|
|
101
|
+
id: string;
|
|
102
|
+
date: string;
|
|
103
|
+
duration: string | null;
|
|
104
|
+
types: SessionType[];
|
|
105
|
+
summary: string;
|
|
106
|
+
relatedRfcs: string[];
|
|
107
|
+
relatedArtifacts: string[];
|
|
108
|
+
decisions: string[];
|
|
109
|
+
commits: string[];
|
|
110
|
+
files: string[];
|
|
111
|
+
commands: string[];
|
|
112
|
+
systemDelta?: SessionSystemDelta;
|
|
113
|
+
diagrams?: SessionDiagram[];
|
|
114
|
+
evidence?: SessionEvidenceEntry[];
|
|
115
|
+
remainingIssues?: string[];
|
|
116
|
+
checkpoint?: SessionCheckpoint;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── session.save ──
|
|
120
|
+
|
|
121
|
+
export interface SessionSaveResult {
|
|
122
|
+
command: "session.save";
|
|
123
|
+
status: "ok";
|
|
124
|
+
file: string;
|
|
125
|
+
rawFile: string;
|
|
126
|
+
rawDeleted: boolean;
|
|
127
|
+
id: string;
|
|
128
|
+
types: SessionType[];
|
|
129
|
+
extractedMetadata: {
|
|
130
|
+
relatedRfcs: string[];
|
|
131
|
+
relatedArtifacts: string[];
|
|
132
|
+
commits: string[];
|
|
133
|
+
files: string[];
|
|
134
|
+
commands: string[];
|
|
135
|
+
};
|
|
136
|
+
dryRun: boolean;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface SessionSaveSkip {
|
|
140
|
+
rawFile: string;
|
|
141
|
+
reason: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── session.archive ──
|
|
145
|
+
|
|
146
|
+
export interface SessionArchiveMove {
|
|
147
|
+
id: string;
|
|
148
|
+
file: string;
|
|
149
|
+
from: string;
|
|
150
|
+
to: string;
|
|
151
|
+
ageDays: number;
|
|
152
|
+
direction: "into-archive" | "out-of-archive";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface SessionArchiveSkip {
|
|
156
|
+
id: string;
|
|
157
|
+
file: string;
|
|
158
|
+
reason: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface SessionArchiveResult {
|
|
162
|
+
command: "session.archive";
|
|
163
|
+
status: "ok";
|
|
164
|
+
moved: SessionArchiveMove[];
|
|
165
|
+
skipped: SessionArchiveSkip[];
|
|
166
|
+
maxAgeDays: number;
|
|
167
|
+
dryRun: boolean;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── session.validate ──
|
|
171
|
+
|
|
172
|
+
export interface SessionValidationViolation {
|
|
173
|
+
rule: SesRule;
|
|
174
|
+
file: string;
|
|
175
|
+
message: string;
|
|
176
|
+
severity: "error" | "warning";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface SessionValidationResult {
|
|
180
|
+
command: "session.validate";
|
|
181
|
+
status: "pass" | "fail";
|
|
182
|
+
violations: SessionValidationViolation[];
|
|
183
|
+
checked: number;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── session.list ──
|
|
187
|
+
|
|
188
|
+
export interface SessionListEntry {
|
|
189
|
+
id: string;
|
|
190
|
+
date: string;
|
|
191
|
+
types: SessionType[];
|
|
192
|
+
summary: string;
|
|
193
|
+
relatedRfcs: string[];
|
|
194
|
+
file: string;
|
|
195
|
+
archived: boolean;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface SessionListResult {
|
|
199
|
+
command: "session.list";
|
|
200
|
+
status: "ok";
|
|
201
|
+
sessions: SessionListEntry[];
|
|
202
|
+
count: number;
|
|
203
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Unit tests for spec.live.list and spec.live.validate handlers (RFC-0711).</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not test merge — covered in live-spec-merge.test.ts.</item>
|
|
6
|
+
</non-goals>
|
|
7
|
+
</MODULE_CONTRACT>
|
|
8
|
+
<CHANGE_SUMMARY>
|
|
9
|
+
<item>RFC-0711: initial unit tests for spec.live.list and spec.live.validate.</item>
|
|
10
|
+
</CHANGE_SUMMARY>
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
14
|
+
import fs from "node:fs/promises";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import os from "node:os";
|
|
17
|
+
import { runSpecLiveList } from "./live-spec-list.ts";
|
|
18
|
+
import { runSpecLiveShow } from "./live-spec-show.ts";
|
|
19
|
+
import { runSpecLiveValidate } from "./live-spec-validate.ts";
|
|
20
|
+
import type { ForgeCommandInput, ForgeRuntimeContext } from "../../src/types.ts";
|
|
21
|
+
|
|
22
|
+
function makeContext(workspaceRoot: string): ForgeRuntimeContext {
|
|
23
|
+
return {
|
|
24
|
+
workspaceRoot,
|
|
25
|
+
logger: {
|
|
26
|
+
section: () => {},
|
|
27
|
+
info: () => {},
|
|
28
|
+
warn: () => {},
|
|
29
|
+
error: () => {},
|
|
30
|
+
success: () => {},
|
|
31
|
+
},
|
|
32
|
+
dryRun: false,
|
|
33
|
+
outputFormat: "json",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const VALID_SPEC = `---
|
|
38
|
+
domain: forge
|
|
39
|
+
title: "Living Spec: forge"
|
|
40
|
+
lastMergedRfc: RFC-9001
|
|
41
|
+
updatedAt: 2026-08-06
|
|
42
|
+
createdAt: 2026-08-06
|
|
43
|
+
history:
|
|
44
|
+
- rfc: RFC-9001
|
|
45
|
+
mergedAt: 2026-08-06
|
|
46
|
+
operation: created
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
# Living Spec: forge
|
|
50
|
+
|
|
51
|
+
## Overview
|
|
52
|
+
|
|
53
|
+
Some content.
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
describe("spec.live.list", () => {
|
|
57
|
+
let tmpDir: string;
|
|
58
|
+
|
|
59
|
+
beforeEach(async () => {
|
|
60
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "spec-live-list-"));
|
|
61
|
+
const liveDir = path.join(tmpDir, "docs/specs/live");
|
|
62
|
+
await fs.mkdir(liveDir, { recursive: true });
|
|
63
|
+
await fs.writeFile(path.join(liveDir, "forge.md"), VALID_SPEC);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
afterEach(async () => {
|
|
67
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("lists living specs", async () => {
|
|
71
|
+
const input: ForgeCommandInput = { argv: [], flags: {} };
|
|
72
|
+
const result = await runSpecLiveList(input, makeContext(tmpDir));
|
|
73
|
+
|
|
74
|
+
expect(result.exitCode).toBe(0);
|
|
75
|
+
expect(result.data?.livingSpecs.length).toBe(1);
|
|
76
|
+
expect(result.data?.livingSpecs[0]?.domain).toBe("forge");
|
|
77
|
+
expect(result.data?.livingSpecs[0]?.lastMergedRfc).toBe("RFC-9001");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("returns empty array when directory does not exist", async () => {
|
|
81
|
+
const emptyDir = await fs.mkdtemp(path.join(os.tmpdir(), "spec-live-empty-"));
|
|
82
|
+
try {
|
|
83
|
+
const input: ForgeCommandInput = { argv: [], flags: {} };
|
|
84
|
+
const result = await runSpecLiveList(input, makeContext(emptyDir));
|
|
85
|
+
|
|
86
|
+
expect(result.exitCode).toBe(0);
|
|
87
|
+
expect(result.data?.livingSpecs.length).toBe(0);
|
|
88
|
+
} finally {
|
|
89
|
+
await fs.rm(emptyDir, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("spec.live.show", () => {
|
|
95
|
+
let tmpDir: string;
|
|
96
|
+
|
|
97
|
+
beforeEach(async () => {
|
|
98
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "spec-live-show-"));
|
|
99
|
+
const liveDir = path.join(tmpDir, "docs/specs/live");
|
|
100
|
+
await fs.mkdir(liveDir, { recursive: true });
|
|
101
|
+
await fs.writeFile(path.join(liveDir, "forge.md"), VALID_SPEC);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
afterEach(async () => {
|
|
105
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("shows a living spec by domain", async () => {
|
|
109
|
+
const input: ForgeCommandInput = { argv: [], flags: { domain: "forge" } };
|
|
110
|
+
const result = await runSpecLiveShow(input, makeContext(tmpDir));
|
|
111
|
+
|
|
112
|
+
expect(result.exitCode).toBe(0);
|
|
113
|
+
expect(result.data?.domain).toBe("forge");
|
|
114
|
+
expect(result.data?.title).toBe("Living Spec: forge");
|
|
115
|
+
expect(result.data?.body).toContain("# Living Spec: forge");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("returns error when domain is missing", async () => {
|
|
119
|
+
const input: ForgeCommandInput = { argv: [], flags: {} };
|
|
120
|
+
const result = await runSpecLiveShow(input, makeContext(tmpDir));
|
|
121
|
+
|
|
122
|
+
expect(result.exitCode).toBe(1);
|
|
123
|
+
expect(result.summary).toContain("--domain");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("returns error when spec not found", async () => {
|
|
127
|
+
const input: ForgeCommandInput = { argv: [], flags: { domain: "nonexistent" } };
|
|
128
|
+
const result = await runSpecLiveShow(input, makeContext(tmpDir));
|
|
129
|
+
|
|
130
|
+
expect(result.exitCode).toBe(1);
|
|
131
|
+
expect(result.summary).toContain("not found");
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("spec.live.validate", () => {
|
|
136
|
+
let tmpDir: string;
|
|
137
|
+
|
|
138
|
+
beforeEach(async () => {
|
|
139
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "spec-live-validate-"));
|
|
140
|
+
const rfcDir = path.join(tmpDir, "docs/rfcs");
|
|
141
|
+
const liveDir = path.join(tmpDir, "docs/specs/live");
|
|
142
|
+
await fs.mkdir(rfcDir, { recursive: true });
|
|
143
|
+
await fs.mkdir(liveDir, { recursive: true });
|
|
144
|
+
|
|
145
|
+
// Create an implemented RFC so V-LS-03/04 pass
|
|
146
|
+
const rfcContent = `---
|
|
147
|
+
id: RFC-9001
|
|
148
|
+
title: "Test"
|
|
149
|
+
status: implemented
|
|
150
|
+
kind: architecture
|
|
151
|
+
scope: workspace
|
|
152
|
+
owners:
|
|
153
|
+
- architecture
|
|
154
|
+
reviewers:
|
|
155
|
+
- human:test
|
|
156
|
+
createdAt: 2026-08-06
|
|
157
|
+
updatedAt: 2026-08-06
|
|
158
|
+
implementedAt: 2026-08-06
|
|
159
|
+
versionBump: patch
|
|
160
|
+
packagesImpacted: []
|
|
161
|
+
commands:
|
|
162
|
+
proposed: []
|
|
163
|
+
added: []
|
|
164
|
+
changed: []
|
|
165
|
+
removed: []
|
|
166
|
+
appsImpacted: []
|
|
167
|
+
successSignals: []
|
|
168
|
+
nonGoals: []
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
# RFC-9001: Test
|
|
172
|
+
|
|
173
|
+
## Design
|
|
174
|
+
|
|
175
|
+
### Something
|
|
176
|
+
|
|
177
|
+
Content.
|
|
178
|
+
`;
|
|
179
|
+
await fs.writeFile(path.join(rfcDir, "rfc-9001-test.md"), rfcContent);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
afterEach(async () => {
|
|
183
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("passes with zero violations on a valid living spec", async () => {
|
|
187
|
+
await fs.writeFile(
|
|
188
|
+
path.join(tmpDir, "docs/specs/live/forge.md"),
|
|
189
|
+
VALID_SPEC,
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const input: ForgeCommandInput = { argv: [], flags: {} };
|
|
193
|
+
const result = await runSpecLiveValidate(input, makeContext(tmpDir));
|
|
194
|
+
|
|
195
|
+
expect(result.exitCode).toBe(0);
|
|
196
|
+
expect(result.data?.status).toBe("pass");
|
|
197
|
+
expect(result.data?.violations.length).toBe(0);
|
|
198
|
+
expect(result.data?.specsChecked).toBe(1);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("passes when directory does not exist (empty)", async () => {
|
|
202
|
+
const emptyDir = await fs.mkdtemp(path.join(os.tmpdir(), "spec-live-validate-empty-"));
|
|
203
|
+
try {
|
|
204
|
+
const input: ForgeCommandInput = { argv: [], flags: {} };
|
|
205
|
+
const result = await runSpecLiveValidate(input, makeContext(emptyDir));
|
|
206
|
+
|
|
207
|
+
expect(result.exitCode).toBe(0);
|
|
208
|
+
expect(result.data?.status).toBe("pass");
|
|
209
|
+
expect(result.data?.specsChecked).toBe(0);
|
|
210
|
+
} finally {
|
|
211
|
+
await fs.rm(emptyDir, { recursive: true, force: true });
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("fails when required frontmatter field is missing", async () => {
|
|
216
|
+
const invalidSpec = `---
|
|
217
|
+
domain: forge
|
|
218
|
+
title: "Living Spec: forge"
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
# Living Spec: forge
|
|
222
|
+
`;
|
|
223
|
+
await fs.writeFile(path.join(tmpDir, "docs/specs/live/forge.md"), invalidSpec);
|
|
224
|
+
|
|
225
|
+
const input: ForgeCommandInput = { argv: [], flags: {} };
|
|
226
|
+
const result = await runSpecLiveValidate(input, makeContext(tmpDir));
|
|
227
|
+
|
|
228
|
+
expect(result.exitCode).toBe(1);
|
|
229
|
+
expect(result.data?.status).toBe("fail");
|
|
230
|
+
expect(result.data?.violations.length).toBeGreaterThan(0);
|
|
231
|
+
expect(result.data?.violations.some((v) => v.rule === "V-LS-01")).toBe(true);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("fails when domain does not match filename (V-LS-02)", async () => {
|
|
235
|
+
const mismatchSpec = `---
|
|
236
|
+
domain: wrong-domain
|
|
237
|
+
title: "Living Spec"
|
|
238
|
+
lastMergedRfc: RFC-9001
|
|
239
|
+
updatedAt: 2026-08-06
|
|
240
|
+
createdAt: 2026-08-06
|
|
241
|
+
history:
|
|
242
|
+
- rfc: RFC-9001
|
|
243
|
+
mergedAt: 2026-08-06
|
|
244
|
+
operation: created
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
# Living Spec
|
|
248
|
+
`;
|
|
249
|
+
await fs.writeFile(path.join(tmpDir, "docs/specs/live/forge.md"), mismatchSpec);
|
|
250
|
+
|
|
251
|
+
const input: ForgeCommandInput = { argv: [], flags: {} };
|
|
252
|
+
const result = await runSpecLiveValidate(input, makeContext(tmpDir));
|
|
253
|
+
|
|
254
|
+
expect(result.exitCode).toBe(1);
|
|
255
|
+
expect(result.data?.violations.some((v) => v.rule === "V-LS-02")).toBe(true);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>spec.live.list handler — lists all living specs in docs/specs/live/ (RFC-0711).</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not merge or validate — use spec.live.merge / spec.live.validate.</item>
|
|
6
|
+
</non-goals>
|
|
7
|
+
</MODULE_CONTRACT>
|
|
8
|
+
<CHANGE_SUMMARY>
|
|
9
|
+
<item>RFC-0711: initial spec.live.list handler.</item>
|
|
10
|
+
</CHANGE_SUMMARY>
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from "node:fs/promises";
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import YAML from "yaml";
|
|
17
|
+
import type {
|
|
18
|
+
ForgeCommandInput,
|
|
19
|
+
ForgeCommandResult,
|
|
20
|
+
ForgeRuntimeContext,
|
|
21
|
+
} from "../../src/types.ts";
|
|
22
|
+
import type { SpecLiveListResult, SpecLiveListEntry } from "./live-spec-types.ts";
|
|
23
|
+
|
|
24
|
+
const LIVE_SPECS_DIR = "docs/specs/live";
|
|
25
|
+
|
|
26
|
+
export async function runSpecLiveList(
|
|
27
|
+
_input: ForgeCommandInput,
|
|
28
|
+
context: ForgeRuntimeContext,
|
|
29
|
+
): Promise<ForgeCommandResult<SpecLiveListResult>> {
|
|
30
|
+
const { workspaceRoot, logger, outputFormat } = context;
|
|
31
|
+
const liveSpecsDir = path.join(workspaceRoot, LIVE_SPECS_DIR);
|
|
32
|
+
|
|
33
|
+
const entries: SpecLiveListEntry[] = [];
|
|
34
|
+
|
|
35
|
+
if (existsSync(liveSpecsDir)) {
|
|
36
|
+
const files = await fs.readdir(liveSpecsDir);
|
|
37
|
+
for (const file of files) {
|
|
38
|
+
if (!file.endsWith(".md") || file === "README.md") continue;
|
|
39
|
+
const filePath = path.join(liveSpecsDir, file);
|
|
40
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
41
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
42
|
+
if (!match) continue;
|
|
43
|
+
const fm = (YAML.parse(match[1]!) ?? {}) as Record<string, unknown>;
|
|
44
|
+
entries.push({
|
|
45
|
+
domain: String(fm["domain"] ?? ""),
|
|
46
|
+
title: String(fm["title"] ?? ""),
|
|
47
|
+
lastMergedRfc: String(fm["lastMergedRfc"] ?? ""),
|
|
48
|
+
updatedAt: String(fm["updatedAt"] ?? ""),
|
|
49
|
+
historyCount: Array.isArray(fm["history"]) ? fm["history"].length : 0,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
entries.sort((a, b) => a.domain.localeCompare(b.domain));
|
|
55
|
+
|
|
56
|
+
const result: SpecLiveListResult = {
|
|
57
|
+
command: "spec.live.list",
|
|
58
|
+
status: "ok",
|
|
59
|
+
livingSpecs: entries,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (outputFormat === "pretty") {
|
|
63
|
+
if (entries.length === 0) {
|
|
64
|
+
logger.info("spec.live.list: no living specs found");
|
|
65
|
+
} else {
|
|
66
|
+
logger.section(`Living Specs (${entries.length})`);
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
logger.info(` ${entry.domain} — ${entry.title} (last: ${entry.lastMergedRfc})`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
data: result,
|
|
75
|
+
exitCode: 0,
|
|
76
|
+
summary: `spec.live.list: ${entries.length} living spec(s)`,
|
|
77
|
+
};
|
|
78
|
+
}
|