@warpgogol/forge 5.3.4 → 6.2.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 +8 -0
- package/bin/cli.ts +7 -2
- package/dist/bin/cli.js +7 -2
- package/dist/bin/cli.js.map +1 -1
- package/dist/os/adr/adr.module.d.ts.map +1 -1
- package/dist/os/adr/adr.module.js +2 -0
- package/dist/os/adr/adr.module.js.map +1 -1
- package/dist/os/audit/audit.module.d.ts.map +1 -1
- package/dist/os/audit/audit.module.js +2 -0
- package/dist/os/audit/audit.module.js.map +1 -1
- package/dist/os/compass/compass.module.d.ts.map +1 -1
- package/dist/os/compass/compass.module.js +47 -15
- package/dist/os/compass/compass.module.js.map +1 -1
- package/dist/os/compass/handlers/compass-audit-handler.d.ts +2 -0
- package/dist/os/compass/handlers/compass-audit-handler.d.ts.map +1 -1
- package/dist/os/compass/handlers/compass-audit-handler.js +40 -10
- package/dist/os/compass/handlers/compass-audit-handler.js.map +1 -1
- package/dist/os/compass/handlers/compass-docs-validate.d.ts +13 -0
- package/dist/os/compass/handlers/compass-docs-validate.d.ts.map +1 -0
- package/dist/os/compass/handlers/compass-docs-validate.js +328 -0
- package/dist/os/compass/handlers/compass-docs-validate.js.map +1 -0
- package/dist/os/compass/handlers/summary-record.d.ts +11 -2
- package/dist/os/compass/handlers/summary-record.d.ts.map +1 -1
- package/dist/os/compass/handlers/summary-record.js +95 -15
- package/dist/os/compass/handlers/summary-record.js.map +1 -1
- package/dist/os/core/core.module.d.ts.map +1 -1
- package/dist/os/core/core.module.js +124 -73
- package/dist/os/core/core.module.js.map +1 -1
- package/dist/os/core/handlers/file-size-lint.d.ts.map +1 -1
- package/dist/os/core/handlers/file-size-lint.js +2 -0
- package/dist/os/core/handlers/file-size-lint.js.map +1 -1
- package/dist/os/exploration/exploration.module.d.ts.map +1 -1
- package/dist/os/exploration/exploration.module.js +3 -0
- package/dist/os/exploration/exploration.module.js.map +1 -1
- package/dist/os/naming/naming-convention.d.ts.map +1 -1
- package/dist/os/naming/naming-convention.js +11 -1
- package/dist/os/naming/naming-convention.js.map +1 -1
- package/dist/os/naming/naming.module.d.ts.map +1 -1
- package/dist/os/naming/naming.module.js +9 -2
- package/dist/os/naming/naming.module.js.map +1 -1
- package/dist/os/notes/notes.module.d.ts.map +1 -1
- package/dist/os/notes/notes.module.js +17 -6
- package/dist/os/notes/notes.module.js.map +1 -1
- package/dist/os/plan/plan.module.d.ts.map +1 -1
- package/dist/os/plan/plan.module.js +2 -0
- package/dist/os/plan/plan.module.js.map +1 -1
- package/dist/os/plugin/plugin.module.d.ts.map +1 -1
- package/dist/os/plugin/plugin.module.js +9 -2
- package/dist/os/plugin/plugin.module.js.map +1 -1
- package/dist/os/program/program.module.d.ts.map +1 -1
- package/dist/os/program/program.module.js +19 -4
- package/dist/os/program/program.module.js.map +1 -1
- package/dist/os/queue/handlers/queue-validate.d.ts.map +1 -1
- package/dist/os/queue/handlers/queue-validate.js +154 -7
- package/dist/os/queue/handlers/queue-validate.js.map +1 -1
- package/dist/os/queue/manifest.d.ts +17 -1
- package/dist/os/queue/manifest.d.ts.map +1 -1
- package/dist/os/queue/manifest.js +127 -6
- package/dist/os/queue/manifest.js.map +1 -1
- package/dist/os/queue/queue.module.d.ts.map +1 -1
- package/dist/os/queue/queue.module.js +32 -4
- package/dist/os/queue/queue.module.js.map +1 -1
- package/dist/os/queue/types.d.ts +100 -1
- package/dist/os/queue/types.d.ts.map +1 -1
- package/dist/os/queue/types.js +44 -0
- package/dist/os/queue/types.js.map +1 -1
- package/dist/os/rfc/handlers/implement-stamp.d.ts.map +1 -1
- package/dist/os/rfc/handlers/implement-stamp.js +30 -8
- package/dist/os/rfc/handlers/implement-stamp.js.map +1 -1
- package/dist/os/rfc/rfc.module.d.ts.map +1 -1
- package/dist/os/rfc/rfc.module.js +25 -6
- package/dist/os/rfc/rfc.module.js.map +1 -1
- package/dist/os/rfc/types.d.ts +12 -2
- package/dist/os/rfc/types.d.ts.map +1 -1
- package/dist/os/rfc/types.js +2 -2
- package/dist/os/rfc/types.js.map +1 -1
- package/dist/os/rfc/verification-evidence.d.ts +21 -0
- package/dist/os/rfc/verification-evidence.d.ts.map +1 -1
- package/dist/os/rfc/verification-evidence.js +134 -23
- package/dist/os/rfc/verification-evidence.js.map +1 -1
- package/dist/os/rfc/verification-refresh.d.ts.map +1 -1
- package/dist/os/rfc/verification-refresh.js +66 -37
- package/dist/os/rfc/verification-refresh.js.map +1 -1
- package/dist/os/session/handlers/save.d.ts.map +1 -1
- package/dist/os/session/handlers/save.js +42 -5
- package/dist/os/session/handlers/save.js.map +1 -1
- package/dist/os/session/session.module.d.ts.map +1 -1
- package/dist/os/session/session.module.js +5 -0
- package/dist/os/session/session.module.js.map +1 -1
- package/dist/os/spec/live-spec-merge.d.ts.map +1 -1
- package/dist/os/spec/live-spec-merge.js +55 -134
- package/dist/os/spec/live-spec-merge.js.map +1 -1
- package/dist/os/spec/live-spec-rebuild.d.ts +32 -0
- package/dist/os/spec/live-spec-rebuild.d.ts.map +1 -0
- package/dist/os/spec/live-spec-rebuild.js +272 -0
- package/dist/os/spec/live-spec-rebuild.js.map +1 -0
- package/dist/os/spec/live-spec-shared.d.ts +62 -0
- package/dist/os/spec/live-spec-shared.d.ts.map +1 -0
- package/dist/os/spec/live-spec-shared.js +315 -0
- package/dist/os/spec/live-spec-shared.js.map +1 -0
- package/dist/os/spec/live-spec-types.d.ts +17 -1
- package/dist/os/spec/live-spec-types.d.ts.map +1 -1
- package/dist/os/spec/live-spec-types.js +3 -0
- package/dist/os/spec/live-spec-types.js.map +1 -1
- package/dist/os/spec/live-spec-validate.d.ts.map +1 -1
- package/dist/os/spec/live-spec-validate.js +105 -2
- package/dist/os/spec/live-spec-validate.js.map +1 -1
- package/dist/os/spec/spec-materialize.d.ts +2 -0
- package/dist/os/spec/spec-materialize.d.ts.map +1 -1
- package/dist/os/spec/spec-materialize.js +45 -5
- package/dist/os/spec/spec-materialize.js.map +1 -1
- package/dist/os/spec/spec-schema.d.ts +19 -0
- package/dist/os/spec/spec-schema.d.ts.map +1 -1
- package/dist/os/spec/spec-schema.js +30 -0
- package/dist/os/spec/spec-schema.js.map +1 -1
- package/dist/os/spec/spec-status.d.ts +4 -0
- package/dist/os/spec/spec-status.d.ts.map +1 -1
- package/dist/os/spec/spec-status.js +19 -1
- package/dist/os/spec/spec-status.js.map +1 -1
- package/dist/os/spec/spec-validate.d.ts.map +1 -1
- package/dist/os/spec/spec-validate.js +55 -8
- package/dist/os/spec/spec-validate.js.map +1 -1
- package/dist/os/spec/spec.module.d.ts.map +1 -1
- package/dist/os/spec/spec.module.js +84 -8
- package/dist/os/spec/spec.module.js.map +1 -1
- package/dist/os/werkstatt/handlers/werkstatt-operation-validate.d.ts.map +1 -1
- package/dist/os/werkstatt/handlers/werkstatt-operation-validate.js +10 -4
- package/dist/os/werkstatt/handlers/werkstatt-operation-validate.js.map +1 -1
- package/dist/os/werkstatt/werkstatt.module.d.ts.map +1 -1
- package/dist/os/werkstatt/werkstatt.module.js +13 -4
- package/dist/os/werkstatt/werkstatt.module.js.map +1 -1
- package/dist/os/workflow/workflow.module.d.ts.map +1 -1
- package/dist/os/workflow/workflow.module.js +13 -5
- package/dist/os/workflow/workflow.module.js.map +1 -1
- package/dist/src/cli-flags.d.ts.map +1 -1
- package/dist/src/cli-flags.js +25 -5
- package/dist/src/cli-flags.js.map +1 -1
- package/dist/src/config/forge-config.d.ts +3 -0
- package/dist/src/config/forge-config.d.ts.map +1 -1
- package/dist/src/config/forge-config.js +2 -0
- package/dist/src/config/forge-config.js.map +1 -1
- package/dist/src/onboarding/doctor.d.ts.map +1 -1
- package/dist/src/onboarding/doctor.js +2 -3
- package/dist/src/onboarding/doctor.js.map +1 -1
- package/dist/src/onboarding/init.d.ts.map +1 -1
- package/dist/src/onboarding/init.js +8 -3
- package/dist/src/onboarding/init.js.map +1 -1
- package/dist/src/pipeline-status.d.ts +14 -1
- package/dist/src/pipeline-status.d.ts.map +1 -1
- package/dist/src/pipeline-status.js +28 -8
- package/dist/src/pipeline-status.js.map +1 -1
- package/dist/src/types.d.ts +7 -1
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +6 -0
- package/dist/src/types.js.map +1 -1
- package/dist/src/utils/generated-marker.d.ts +8 -0
- package/dist/src/utils/generated-marker.d.ts.map +1 -1
- package/dist/src/utils/generated-marker.js +25 -0
- package/dist/src/utils/generated-marker.js.map +1 -1
- package/dist/src/validators/skill-validate.d.ts.map +1 -1
- package/dist/src/validators/skill-validate.js +2 -4
- package/dist/src/validators/skill-validate.js.map +1 -1
- package/os/adr/adr.module.ts +2 -0
- package/os/audit/audit.module.ts +2 -0
- package/os/compass/compass.module.ts +49 -15
- package/os/compass/handlers/compass-audit-handler.ts +51 -10
- package/os/compass/handlers/compass-docs-validate.ts +361 -0
- package/os/compass/handlers/summary-record.ts +106 -15
- package/os/compass/handlers/tests/compass-audit-record.test.ts +2 -1
- package/os/compass/handlers/tests/compass-audit-validate.test.ts +92 -3
- package/os/compass/handlers/tests/compass-docs-validate.test.ts +203 -0
- package/os/compass/handlers/tests/compass-ledger-scope.test.ts +2 -1
- package/os/compass/handlers/tests/compass-policy.test.ts +7 -1
- package/os/compass/handlers/tests/summary-record.test.ts +127 -0
- package/os/core/core.module.ts +147 -75
- package/os/core/docs-archive.test.ts +136 -0
- package/os/core/handlers/file-size-lint.ts +2 -0
- package/os/exploration/exploration.module.ts +3 -0
- package/os/naming/naming-convention.ts +11 -1
- package/os/naming/naming.module.ts +9 -2
- package/os/notes/notes.module.ts +17 -6
- package/os/plan/plan-0000-template.md +150 -0
- package/os/plan/plan.module.ts +2 -0
- package/os/plugin/plugin.module.ts +9 -2
- package/os/program/program.module.ts +19 -4
- package/os/queue/decision-ledger.test.ts +163 -0
- package/os/queue/handlers/queue-validate.ts +190 -8
- package/os/queue/manifest.ts +201 -7
- package/os/queue/queue-validate.test.ts +303 -0
- package/os/queue/queue.module.ts +32 -4
- package/os/queue/types.ts +56 -1
- package/os/rfc/handlers/implement-stamp.ts +37 -8
- package/os/rfc/rfc-read-only-no-side-effects.test.ts +1 -0
- package/os/rfc/rfc.module.ts +27 -6
- package/os/rfc/types.ts +18 -3
- package/os/rfc/verification-emit.test.ts +305 -0
- package/os/rfc/verification-evidence.ts +149 -24
- package/os/rfc/verification-refresh.test.ts +222 -1
- package/os/rfc/verification-refresh.ts +74 -38
- package/os/session/handlers/save.ts +37 -5
- package/os/session/session.module.ts +5 -0
- package/os/spec/live-spec-idempotency.pbt.test.ts +222 -0
- package/os/spec/live-spec-list-show-validate.test.ts +10 -6
- package/os/spec/live-spec-merge.test.ts +187 -7
- package/os/spec/live-spec-merge.ts +71 -155
- package/os/spec/live-spec-rebuild.test.ts +317 -0
- package/os/spec/live-spec-rebuild.ts +356 -0
- package/os/spec/live-spec-shared.ts +378 -0
- package/os/spec/live-spec-types.ts +21 -1
- package/os/spec/live-spec-validate.test.ts +317 -0
- package/os/spec/live-spec-validate.ts +106 -2
- package/os/spec/spec-materialize.test.ts +192 -0
- package/os/spec/spec-materialize.ts +48 -4
- package/os/spec/spec-schema.ts +49 -0
- package/os/spec/spec-status.ts +24 -0
- package/os/spec/spec-validate.test.ts +66 -0
- package/os/spec/spec-validate.ts +62 -8
- package/os/spec/spec.module.ts +85 -8
- package/os/werkstatt/handlers/werkstatt-operation-validate.ts +10 -4
- package/os/werkstatt/werkstatt.module.ts +13 -4
- package/os/workflow/workflow.module.ts +13 -5
- package/package.json +1 -1
- package/skills/_shared/fo-pipeline-conventions.md +126 -1
- package/skills/fo/fo-doc-audit/SKILL.md +4 -4
- package/skills/fo/fo-explore/SKILL.md +2 -1
- package/skills/fo/fo-fix/SKILL.md +11 -3
- package/skills/fo/fo-handoff/SKILL.md +4 -6
- package/skills/fo/fo-idea-create-rfc/SKILL.md +2 -1
- package/skills/fo/fo-idea-enhance/SKILL.md +23 -4
- package/skills/fo/fo-idea-i-just-want-to-see-the-result/SKILL.md +86 -22
- package/skills/fo/fo-idea-implement/SKILL.md +53 -10
- package/skills/fo/fo-idea-plan/SKILL.md +28 -6
- package/skills/fo/fo-review/SKILL.md +69 -8
- package/skills/fo/fo-session-retro/SKILL.md +3 -2
- package/skills/fo/fo-step-commit/SKILL.md +2 -0
- package/skills/shared/grilling/SKILL.md +12 -0
- package/skills/shared/writing-great-skills/GLOSSARY.md +2 -0
- package/skills/shared/writing-great-skills/SKILL.md +2 -1
- package/src/cli-flags.ts +23 -5
- package/src/config/forge-config.ts +4 -0
- package/src/onboarding/doctor.ts +2 -3
- package/src/onboarding/init.ts +8 -3
- package/src/pipeline-status.ts +32 -8
- package/src/tests/cli-flags.test.ts +20 -0
- package/src/tests/cli-output.test.ts +13 -7
- package/src/tests/fixtures/agents-generate-business-before.txt +7 -7
- package/src/tests/implement-stamp.test.ts +38 -0
- package/src/tests/session-handlers.test.ts +33 -0
- package/src/types.ts +13 -1
- package/src/utils/generated-marker.ts +28 -0
- package/src/validators/skill-validate.ts +2 -4
- package/skills/fo/fo-idea-implement/ADR-FLOW.md +0 -179
- package/skills/fo/fo-review/AXES.md +0 -70
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>RFC-1242: validate that the live root docs/*.xml corpus describes the repository as it exists — every path token, workspace id, and knowledge-graph link target must resolve to a real file, directory, or declared node.</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not scan archived trees, vendored specs, sessions/metrics, or authored RFC/ADR/plan bodies — frozen history and forward-looking prose legitimately name paths that do not exist.</item>
|
|
6
|
+
<item>Do not validate markdown link targets — only the XML semantic layer is gated (markdown sweep is manual).</item>
|
|
7
|
+
<item>Do not mutate anything — this handler is read-only; pruning lives on compass.audit.validate --prune.</item>
|
|
8
|
+
</non-goals>
|
|
9
|
+
</MODULE_CONTRACT>
|
|
10
|
+
<KEY_DECISIONS>
|
|
11
|
+
<item>Tag-stack tokenizer over an XML dependency — small authored corpus; a balance check catches literal-tag breakage (COMPASS-DOC-00).</item>
|
|
12
|
+
<item>Link targets resolve to declared node ids OR file/dir conventions (DOC-03) — dangling-by-convention stays a warning, never an error.</item>
|
|
13
|
+
</KEY_DECISIONS>
|
|
14
|
+
<CHANGE_SUMMARY>
|
|
15
|
+
<item>RFC-1242: review fixes — self-close detection strips quoted attr values first (a="foo/" no longer fakes a self-close), non-backticked path tokens get the same ellipsis strip, docs.* link slugs keep their basename prefix (docs.plans.plan-rfc-* resolves).</item>
|
|
16
|
+
<item>RFC-1242: created — scan docs/*.xml for unresolvable paths, workspace ids, and link targets (COMPASS-DOC-00..03).</item>
|
|
17
|
+
<item>RFC-1249: wire werkstatt.commands.validate into packages.check + retire CMD-OUTPUT debt</item>
|
|
18
|
+
</CHANGE_SUMMARY>
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { resolve } from "node:path";
|
|
22
|
+
import { resolveIo } from "../../../src/utils/io.ts";
|
|
23
|
+
import type { Diagnostic, WorkspaceIO } from "../../../src/types.ts";
|
|
24
|
+
import type {
|
|
25
|
+
ForgeCommandInput,
|
|
26
|
+
ForgeCommandResult,
|
|
27
|
+
ForgeRuntimeContext,
|
|
28
|
+
} from "../../../src/types.ts";
|
|
29
|
+
|
|
30
|
+
const DOCS_GLOB = "*.xml";
|
|
31
|
+
|
|
32
|
+
// Well-formedness: minimal tag-stack check. Skips comments, CDATA, PIs and
|
|
33
|
+
// declarations; a literal `<name` inside prose opens a tag that never balances
|
|
34
|
+
// → COMPASS-DOC-00. The corpus is authored XML without < in attribute values.
|
|
35
|
+
function checkWellFormed(file: string, source: string): { line: number; message: string } | null {
|
|
36
|
+
const stack: Array<{ name: string; line: number }> = [];
|
|
37
|
+
let i = 0;
|
|
38
|
+
let line = 1;
|
|
39
|
+
const advanceLine = (chunk: string) => {
|
|
40
|
+
for (const c of chunk) if (c === "\n") line++;
|
|
41
|
+
};
|
|
42
|
+
while (i < source.length) {
|
|
43
|
+
const open = source.indexOf("<", i);
|
|
44
|
+
if (open === -1) break;
|
|
45
|
+
advanceLine(source.slice(i, open));
|
|
46
|
+
const rest = source.slice(open);
|
|
47
|
+
// comments / CDATA / PI / declarations
|
|
48
|
+
if (rest.startsWith("<!--")) {
|
|
49
|
+
const end = source.indexOf("-->", open + 4);
|
|
50
|
+
if (end === -1) return { line, message: "unterminated comment" };
|
|
51
|
+
advanceLine(source.slice(open, end + 3));
|
|
52
|
+
i = end + 3;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (rest.startsWith("<![CDATA[")) {
|
|
56
|
+
const end = source.indexOf("]]>", open + 9);
|
|
57
|
+
if (end === -1) return { line, message: "unterminated CDATA" };
|
|
58
|
+
advanceLine(source.slice(open, end + 3));
|
|
59
|
+
i = end + 3;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (rest.startsWith("<?")) {
|
|
63
|
+
const end = source.indexOf("?>", open + 2);
|
|
64
|
+
if (end === -1) return { line, message: "unterminated processing instruction" };
|
|
65
|
+
advanceLine(source.slice(open, end + 2));
|
|
66
|
+
i = end + 2;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (rest.startsWith("<!")) {
|
|
70
|
+
const end = source.indexOf(">", open + 2);
|
|
71
|
+
if (end === -1) return { line, message: "unterminated declaration" };
|
|
72
|
+
advanceLine(source.slice(open, end + 1));
|
|
73
|
+
i = end + 1;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const tagMatch = /^<(\/?)([A-Za-z_][\w:.-]*)((?:"[^"]*"|'[^']*'|[^>"'])*)?(\/?)>/.exec(rest);
|
|
77
|
+
if (!tagMatch) {
|
|
78
|
+
// `<` not starting a tag — literal text, tolerated.
|
|
79
|
+
advanceLine(source.slice(open, open + 1));
|
|
80
|
+
i = open + 1;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const [, closing, name, attrs = "", selfClose] = tagMatch;
|
|
84
|
+
advanceLine(tagMatch[0]);
|
|
85
|
+
i = open + tagMatch[0].length;
|
|
86
|
+
if (closing === "/") {
|
|
87
|
+
const top = stack.pop();
|
|
88
|
+
if (!top || top.name !== name) {
|
|
89
|
+
return {
|
|
90
|
+
line,
|
|
91
|
+
message: `mismatched closing tag </${name}>${top ? ` — expected </${top.name}> opened at line ${top.line}` : " — no open tag"}`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const attrsUnquoted = attrs.replace(/"[^"]*"|'[^']*'/g, "");
|
|
97
|
+
if (selfClose !== "/" && !attrsUnquoted.trimEnd().endsWith("/")) {
|
|
98
|
+
stack.push({ name, line });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (stack.length > 0) {
|
|
102
|
+
const top = stack[stack.length - 1]!;
|
|
103
|
+
return { line: top.line, message: `unclosed tag <${top.name}>` };
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
interface ExtractedRefs {
|
|
109
|
+
paths: Array<{ value: string; line: number }>;
|
|
110
|
+
workspaceIds: Array<{ id: string; line: number }>;
|
|
111
|
+
linkTargets: Array<{ target: string; line: number }>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function extractRefs(source: string): ExtractedRefs {
|
|
115
|
+
const refs: ExtractedRefs = { paths: [], workspaceIds: [], linkTargets: [] };
|
|
116
|
+
const lineOf = (index: number) => source.slice(0, index).split("\n").length;
|
|
117
|
+
for (const m of source.matchAll(/<path>([\s\S]*?)<\/path>/g)) {
|
|
118
|
+
refs.paths.push({ value: m[1] ?? "", line: lineOf(m.index) });
|
|
119
|
+
}
|
|
120
|
+
for (const m of source.matchAll(/<workspace\s[^>]*?id="([^"]+)"/g)) {
|
|
121
|
+
refs.workspaceIds.push({ id: m[1]!, line: lineOf(m.index) });
|
|
122
|
+
}
|
|
123
|
+
for (const m of source.matchAll(/<node\s[^>]*>/g)) {
|
|
124
|
+
// DOC-02 only applies to ids that claim a workspace: <workspace> entries
|
|
125
|
+
// and workspace-typed nodes. Layer/symbolic ids (apps.rules,
|
|
126
|
+
// packages.rules, changelog.*) name semantic handles, not directories.
|
|
127
|
+
const id = /\bid="([^"]+)"/.exec(m[0])?.[1];
|
|
128
|
+
const type = /\btype="([^"]+)"/.exec(m[0])?.[1];
|
|
129
|
+
if (id && type === "workspace") {
|
|
130
|
+
refs.workspaceIds.push({ id, line: lineOf(m.index) });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
for (const m of source.matchAll(/<link\s[^>]*?target="([^"]+)"/g)) {
|
|
134
|
+
refs.linkTargets.push({ target: m[1]!, line: lineOf(m.index) });
|
|
135
|
+
}
|
|
136
|
+
return refs;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Normalize a <path> payload into checkable path tokens. Each backticked
|
|
140
|
+
// fragment is one candidate — splitting on commas inside would shred brace
|
|
141
|
+
// expansions (`{a,b}`) and ellipsis ranges (`docs/x/…` … `leaf`). A bare tail
|
|
142
|
+
// fragment after an `…` outside the backticks resolves under the previous
|
|
143
|
+
// fragment's directory (range shorthand for two versions of the same dir).
|
|
144
|
+
function pathTokens(raw: string): string[] {
|
|
145
|
+
const tokens: string[] = [];
|
|
146
|
+
const fragments = [...raw.matchAll(/`([^`]*)`/g)].map((m) => m[1]!.trim());
|
|
147
|
+
if (fragments.length === 0) {
|
|
148
|
+
return raw
|
|
149
|
+
.split(/[\s,]+/)
|
|
150
|
+
.map((t) =>
|
|
151
|
+
t
|
|
152
|
+
.trim()
|
|
153
|
+
.replace(/(?:…|\.\.\.).*$/, "")
|
|
154
|
+
.replace(/\/+$/, ""),
|
|
155
|
+
)
|
|
156
|
+
.filter((t) => t.length > 0 && !t.startsWith("<") && t !== "…" && t !== "+");
|
|
157
|
+
}
|
|
158
|
+
const joinsWithEllipsis = raw.replace(/`[^`]*`/g, "").includes("…");
|
|
159
|
+
let parentDir: string | null = null;
|
|
160
|
+
for (const frag of fragments) {
|
|
161
|
+
const cleaned = frag.replace(/(?:…|\.\.\.).*$/, "").replace(/\/+$/, "");
|
|
162
|
+
if (!cleaned || cleaned === "+") continue;
|
|
163
|
+
if (joinsWithEllipsis && !cleaned.includes("/") && parentDir) {
|
|
164
|
+
tokens.push(`${parentDir}/${cleaned}`);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
tokens.push(cleaned);
|
|
168
|
+
parentDir = cleaned.split("/").slice(0, -1).join("/") || parentDir;
|
|
169
|
+
}
|
|
170
|
+
return tokens;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Directory prefix that must exist for a path token (glob tails trimmed). */
|
|
174
|
+
function pathCheckTarget(token: string): string {
|
|
175
|
+
const globIdx = token.search(/[*{[]/);
|
|
176
|
+
if (globIdx === -1) return token;
|
|
177
|
+
const prefix = token.slice(0, globIdx);
|
|
178
|
+
return prefix.replace(/\/[^/]*$/, "") || ".";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Resolve a workspace-style id to the package/service directory it names. */
|
|
182
|
+
function workspaceIdTarget(id: string): string | null {
|
|
183
|
+
const pkg = /^pkg-([a-z0-9-]+)$/.exec(id);
|
|
184
|
+
if (pkg) return `packages/${pkg[1]}`;
|
|
185
|
+
const svc = /^svc-([a-z0-9-]+)$/.exec(id);
|
|
186
|
+
if (svc) return `services/${svc[1]}`;
|
|
187
|
+
const app = /^app-([a-z0-9-]+)$/.exec(id);
|
|
188
|
+
if (app) return `apps/${app[1]}`;
|
|
189
|
+
const dotted = /^(packages|apps|services)\.([a-z0-9-]+)/.exec(id);
|
|
190
|
+
if (dotted) return `${dotted[1]}/${dotted[2]}`;
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* DOC-03 resolution for dotted link targets: declared node id (checked by the
|
|
196
|
+
* caller) or a file/dir convention — docs.<dir>.<slug> → docs/<dir>/<slug>*,
|
|
197
|
+
* packages./apps./services. → workspace dir, spec.<x> → docs/specs/<x>*,
|
|
198
|
+
* dna-N → docs/architecture-dna.md, dotted command names → command manifest.
|
|
199
|
+
*/
|
|
200
|
+
async function linkTargetResolves(
|
|
201
|
+
target: string,
|
|
202
|
+
declaredIds: Set<string>,
|
|
203
|
+
io: WorkspaceIO,
|
|
204
|
+
docsDir: string,
|
|
205
|
+
commandNames: Set<string>,
|
|
206
|
+
): Promise<"node" | "file" | "none"> {
|
|
207
|
+
if (declaredIds.has(target)) return "node";
|
|
208
|
+
if (commandNames.has(target)) return "file";
|
|
209
|
+
if (/^dna-\d+$/.test(target)) {
|
|
210
|
+
return (await io.exists(resolve(docsDir, "architecture-dna.md"))) ? "file" : "none";
|
|
211
|
+
}
|
|
212
|
+
const docsId = /^docs\.([a-z-]+)\.(.+)$/.exec(target);
|
|
213
|
+
if (docsId) {
|
|
214
|
+
const [, dir, slug] = docsId;
|
|
215
|
+
const hits = await io.glob(`${dir}/**/*${slug}*.md`, { cwd: docsDir }).catch(() => []);
|
|
216
|
+
if (hits.length > 0) return "file";
|
|
217
|
+
// generic fallback: any file whose basename contains the slug under docs/
|
|
218
|
+
const broad = await io.glob(`**/*${slug}*`, { cwd: docsDir }).catch(() => []);
|
|
219
|
+
if (broad.length > 0) return "file";
|
|
220
|
+
return "none";
|
|
221
|
+
}
|
|
222
|
+
const ws = workspaceIdTarget(target);
|
|
223
|
+
if (ws) return (await io.exists(ws)) ? "file" : "none";
|
|
224
|
+
const spec = /^spec\.([a-z0-9-]+)$/.exec(target);
|
|
225
|
+
if (spec) {
|
|
226
|
+
const hits = await io.glob(`specs/${spec[1]}*/**`, { cwd: docsDir }).catch(() => []);
|
|
227
|
+
const dirs = await io.glob(`specs/${spec[1]}*`, { cwd: docsDir }).catch(() => []);
|
|
228
|
+
return hits.length + dirs.length > 0 ? "file" : "none";
|
|
229
|
+
}
|
|
230
|
+
return "none";
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function runCompassDocsValidate(
|
|
234
|
+
input: ForgeCommandInput,
|
|
235
|
+
context: ForgeRuntimeContext,
|
|
236
|
+
): Promise<
|
|
237
|
+
ForgeCommandResult<{
|
|
238
|
+
command: "compass.docs.validate";
|
|
239
|
+
status: "pass" | "fail";
|
|
240
|
+
scanned: { xmlFiles: number; pathsChecked: number; idsChecked: number };
|
|
241
|
+
diagnostics: Diagnostic[];
|
|
242
|
+
}>
|
|
243
|
+
> {
|
|
244
|
+
const io = resolveIo(context.io);
|
|
245
|
+
const docsDir = resolve(context.workspaceRoot, "docs");
|
|
246
|
+
const diagnostics: Diagnostic[] = [];
|
|
247
|
+
let pathsChecked = 0;
|
|
248
|
+
let idsChecked = 0;
|
|
249
|
+
|
|
250
|
+
// Command-name convention for DOC-03: dotted targets like ratgeber.hub.validate
|
|
251
|
+
// resolve when the command registry lists them.
|
|
252
|
+
const commandNames = new Set<string>();
|
|
253
|
+
const manifestPath = resolve(docsDir, "command-manifest.generated.yaml");
|
|
254
|
+
if (await io.exists(manifestPath)) {
|
|
255
|
+
const manifest = await io.readFile(manifestPath);
|
|
256
|
+
for (const m of manifest.matchAll(/^\s*- name: ([\w.-]+)\s*$/gm)) {
|
|
257
|
+
commandNames.add(m[1]!);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const xmlFiles = (await io.glob(DOCS_GLOB, { cwd: docsDir })).sort();
|
|
262
|
+
const declaredNodeIds = new Set<string>();
|
|
263
|
+
|
|
264
|
+
// Pass 1: collect declared node ids so link targets can resolve across files.
|
|
265
|
+
const sources = new Map<string, string>();
|
|
266
|
+
for (const rel of xmlFiles) {
|
|
267
|
+
const abs = resolve(docsDir, rel);
|
|
268
|
+
const source = await io.readFile(abs);
|
|
269
|
+
sources.set(rel, source);
|
|
270
|
+
for (const m of source.matchAll(/<node\s[^>]*?id="([^"]+)"/g)) {
|
|
271
|
+
declaredNodeIds.add(m[1]!);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
for (const rel of xmlFiles) {
|
|
276
|
+
const file = `docs/${rel}`;
|
|
277
|
+
const source = sources.get(rel)!;
|
|
278
|
+
|
|
279
|
+
const malformed = checkWellFormed(file, source);
|
|
280
|
+
if (malformed) {
|
|
281
|
+
diagnostics.push({
|
|
282
|
+
ruleId: "COMPASS-DOC-00",
|
|
283
|
+
severity: "error",
|
|
284
|
+
file,
|
|
285
|
+
line: malformed.line,
|
|
286
|
+
message: `file is not well-formed XML: ${malformed.message}`,
|
|
287
|
+
});
|
|
288
|
+
continue; // refs from a broken file are noise
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const refs = extractRefs(source);
|
|
292
|
+
|
|
293
|
+
for (const { value, line } of refs.paths) {
|
|
294
|
+
for (const token of pathTokens(value)) {
|
|
295
|
+
pathsChecked++;
|
|
296
|
+
const target = pathCheckTarget(token);
|
|
297
|
+
if (!(await io.exists(resolve(context.workspaceRoot, target)))) {
|
|
298
|
+
diagnostics.push({
|
|
299
|
+
ruleId: "COMPASS-DOC-01",
|
|
300
|
+
severity: "error",
|
|
301
|
+
file,
|
|
302
|
+
line,
|
|
303
|
+
message: `<path> references '${token}' — does not exist under the workspace root`,
|
|
304
|
+
fixHint: "Repoint to the live path or remove the stale reference",
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
for (const { id, line } of refs.workspaceIds) {
|
|
311
|
+
const target = workspaceIdTarget(id);
|
|
312
|
+
if (!target) continue;
|
|
313
|
+
idsChecked++;
|
|
314
|
+
if (!(await io.exists(resolve(context.workspaceRoot, target)))) {
|
|
315
|
+
diagnostics.push({
|
|
316
|
+
ruleId: "COMPASS-DOC-02",
|
|
317
|
+
severity: "error",
|
|
318
|
+
file,
|
|
319
|
+
line,
|
|
320
|
+
message: `id '${id}' names workspace path '${target}' — does not exist`,
|
|
321
|
+
fixHint: "Repoint to the live package or remove the stale entry",
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
for (const { target, line } of refs.linkTargets) {
|
|
327
|
+
idsChecked++;
|
|
328
|
+
const resolved = await linkTargetResolves(target, declaredNodeIds, io, docsDir, commandNames);
|
|
329
|
+
if (resolved === "none") {
|
|
330
|
+
diagnostics.push({
|
|
331
|
+
ruleId: "COMPASS-DOC-03",
|
|
332
|
+
severity: "warning",
|
|
333
|
+
file,
|
|
334
|
+
line,
|
|
335
|
+
message: `link target '${target}' resolves to no declared node and no file`,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const errors = diagnostics.filter((d) => d.severity === "error");
|
|
342
|
+
for (const d of diagnostics) {
|
|
343
|
+
context.logger[d.severity === "error" ? "error" : "warn"](
|
|
344
|
+
`[compass.docs.validate] ${d.ruleId}: ${d.file}${d.line ? `:${d.line}` : ""}: ${d.message}`,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
data: {
|
|
350
|
+
command: "compass.docs.validate",
|
|
351
|
+
status: errors.length === 0 ? "pass" : "fail",
|
|
352
|
+
scanned: { xmlFiles: xmlFiles.length, pathsChecked, idsChecked },
|
|
353
|
+
diagnostics,
|
|
354
|
+
},
|
|
355
|
+
exitCode: errors.length > 0 ? 1 : 0,
|
|
356
|
+
summary:
|
|
357
|
+
errors.length === 0
|
|
358
|
+
? `[compass.docs.validate] OK (${xmlFiles.length} files, ${pathsChecked} paths, ${idsChecked} ids)`
|
|
359
|
+
: `[compass.docs.validate] ${errors.length} error(s)`,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
@@ -15,24 +15,18 @@ items to CHANGE_SUMMARY blocks per RFC-1095. Collapses the 5-item window into
|
|
|
15
15
|
<item>Item text is sanitized on record — literal Compass tags would corrupt history parsing.</item>
|
|
16
16
|
</KEY_DECISIONS>
|
|
17
17
|
<CHANGE_SUMMARY>
|
|
18
|
-
<item>RFC-1097: steps 1-4 — compass.migrate codemod
|
|
19
|
-
|
|
20
|
-
Add the v1 to v2 Compass header codemod: migrateFile pure transform (collapse, strip, seed, reorder, purpose-flag actions), migrateWorkspace walker, runCompassMigrate handler with dirty-tree refusal and --force/--files/--dry-run flags, module registration, and 15 unit tests.</item>
|
|
21
|
-
<item>RFC-1097: sweep — packages/forge + services clean
|
|
22
|
-
|
|
23
|
-
Sweep batch 2: real KEY_DECISIONS on 10 files, expanded purposes (CONTRACT-02/PURPOSE-02), headers on mission/index + gen-upstreams, sanitizeItemText in summary.record (literal Compass tags corrupted history), excludedPaths for wrangler types, test-fixtures testPattern. forge+services now 0 diagnostics under --mode error.</item>
|
|
24
|
-
<item>RFC-1097: sweep — engine package clean
|
|
25
|
-
|
|
26
|
-
Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. engine package now 0 diagnostics.</item>
|
|
27
18
|
<item>RFC-1097: AC-4 banned literal in os/compass handlers
|
|
28
19
|
|
|
29
20
|
compass-migrate-handler hint used a consumer-specific run command — switched to generic 'pnpm exec forge run' convention. Reworded recorded CHANGE_SUMMARY items in 3 handlers to drop the consumer-specific literal. compass-policy AC-4 test green (65/65).</item>
|
|
30
21
|
<item>RFC-1220: step 2 — fail-closed guards in history paths</item>
|
|
31
|
-
<
|
|
22
|
+
<item>RFC-1233: stripGitTrailers at the record point + sanitizeItemText star-slash guard — commit-message trailers (Token: value / Token #value, plus markdown-link attribution lines like "Generated with [X](url)") no longer pollute injected items.</item>
|
|
23
|
+
<item>Session-retro 2026-10-08: id-in-diff dedup — auto-inject skips when the worktree diff already carries a same-ID item (HEAD-vs-worktree via git show HEAD:path), closing the descriptive+generic duplicate class seen on RFC-1237.</item>
|
|
24
|
+
<item>RFC-1249: wire werkstatt.commands.validate into packages.check + retire CMD-OUTPUT debt</item>
|
|
25
|
+
<history>CONTRACT-02, PURPOSE-02, RFC-1095, RFC-1097</history>
|
|
32
26
|
</CHANGE_SUMMARY>
|
|
33
27
|
*/
|
|
34
28
|
|
|
35
|
-
import { existsSync } from "../../../src/utils/sync-fs.ts";
|
|
29
|
+
import { execFile, existsSync } from "../../../src/utils/sync-fs.ts";
|
|
36
30
|
import { ambientIo, resolveIo } from "../../../src/utils/io.ts";
|
|
37
31
|
|
|
38
32
|
import { resolve, relative } from "node:path";
|
|
@@ -69,7 +63,7 @@ export interface SummaryRecordInput {
|
|
|
69
63
|
workpiece?: string;
|
|
70
64
|
}
|
|
71
65
|
|
|
72
|
-
export type SummaryRecordSkipReason = "no-block" | "duplicate" | "unparseable";
|
|
66
|
+
export type SummaryRecordSkipReason = "no-block" | "duplicate" | "unparseable" | "id-in-diff";
|
|
73
67
|
|
|
74
68
|
export interface SummaryRecordResult {
|
|
75
69
|
command: "compass.summary.record";
|
|
@@ -167,6 +161,41 @@ export function stripConventionalPrefix(subject: string): string {
|
|
|
167
161
|
return subject.replace(CONVENTIONAL_PREFIX_RE, "").trim();
|
|
168
162
|
}
|
|
169
163
|
|
|
164
|
+
// RFC-1233: git-trailer tail block — strict `Token: value` / `Token #value`
|
|
165
|
+
// lines (interpret-trailers shape, empty value allowed), plus lines ending
|
|
166
|
+
// in a markdown link (the observed "Generated with [X](url)" attribution
|
|
167
|
+
// boilerplate has no separator). A link-shaped line is admitted only when
|
|
168
|
+
// the block also contains at least one strict trailer line — a lone link
|
|
169
|
+
// tail is prose.
|
|
170
|
+
const TRAILER_LINE_RE = /^[A-Za-z0-9-]+(:| #)\s*.*$/;
|
|
171
|
+
const MARKDOWN_LINK_TAIL_RE = /^.*\[[^\]]*\]\([^)]+\)\s*$/;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Remove a trailing git-trailer block: the contiguous tail of strict
|
|
175
|
+
* trailer lines and admitted markdown-link lines. A blank line ends the
|
|
176
|
+
* tail paragraph. Returns the message without the block; when the tail
|
|
177
|
+
* holds no strict trailer line the message is returned unchanged.
|
|
178
|
+
*/
|
|
179
|
+
export function stripGitTrailers(message: string): string {
|
|
180
|
+
const lines = message.trimEnd().split("\n");
|
|
181
|
+
let start = lines.length;
|
|
182
|
+
let sawStrict = false;
|
|
183
|
+
while (start > 0) {
|
|
184
|
+
const line = lines[start - 1]!.trim();
|
|
185
|
+
if (line === "") break;
|
|
186
|
+
if (TRAILER_LINE_RE.test(line)) {
|
|
187
|
+
sawStrict = true;
|
|
188
|
+
start--;
|
|
189
|
+
} else if (MARKDOWN_LINK_TAIL_RE.test(line)) {
|
|
190
|
+
start--;
|
|
191
|
+
} else {
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (!sawStrict || start === lines.length) return message;
|
|
196
|
+
return lines.slice(0, start).join("\n").trimEnd();
|
|
197
|
+
}
|
|
198
|
+
|
|
170
199
|
// Literal Compass block tags inside an item corrupt block parsing — a commit
|
|
171
200
|
// message saying "collapsed into history" would be matched by HISTORY_RE and
|
|
172
201
|
// produce phantom non-ID tokens (CS-07). Strip the angle brackets on record.
|
|
@@ -174,7 +203,14 @@ const COMPASS_TAG_LITERAL_RE =
|
|
|
174
203
|
/<\/?(?:CHANGE_SUMMARY|MODULE_CONTRACT|KEY_DECISIONS|history|item|purpose|non-goals)>/g;
|
|
175
204
|
|
|
176
205
|
export function sanitizeItemText(text: string): string {
|
|
177
|
-
return
|
|
206
|
+
return (
|
|
207
|
+
text
|
|
208
|
+
.replace(COMPASS_TAG_LITERAL_RE, (tag) => tag.replace(/[<>]/g, ""))
|
|
209
|
+
// RFC-1233: a literal star-slash sequence would terminate the
|
|
210
|
+
// surrounding block comment when the item lands in /* ... */ headers.
|
|
211
|
+
// ∕ (U+2215) is visually near-identical, idempotent, and never re-forms.
|
|
212
|
+
.replace(/\*\//g, "*\u2215")
|
|
213
|
+
);
|
|
178
214
|
}
|
|
179
215
|
|
|
180
216
|
export function isValidGovernanceId(id: string, policy: CompassPolicy): boolean {
|
|
@@ -190,6 +226,8 @@ interface RecordOutcome {
|
|
|
190
226
|
/**
|
|
191
227
|
* Append `ID: text` to one file's CHANGE_SUMMARY and collapse the window.
|
|
192
228
|
* Returns the outcome; writes the file only when mutated and not dry-run.
|
|
229
|
+
* `headSource` is the file's content at HEAD (when resolvable) — it enables
|
|
230
|
+
* the id-in-diff dedup; pass undefined outside git workspaces.
|
|
193
231
|
*/
|
|
194
232
|
export async function recordSummaryItem(
|
|
195
233
|
absPath: string,
|
|
@@ -198,6 +236,7 @@ export async function recordSummaryItem(
|
|
|
198
236
|
text: string,
|
|
199
237
|
dryRun: boolean,
|
|
200
238
|
policy: CompassPolicy,
|
|
239
|
+
headSource?: string,
|
|
201
240
|
): Promise<RecordOutcome> {
|
|
202
241
|
const source = await io.readFile(absPath);
|
|
203
242
|
const blockMatch = source.match(CHANGE_SUMMARY_BLOCK_RE);
|
|
@@ -228,6 +267,22 @@ export async function recordSummaryItem(
|
|
|
228
267
|
return { recorded: false, collapsed: false, skipReason: "duplicate" };
|
|
229
268
|
}
|
|
230
269
|
|
|
270
|
+
// A same-ID item already in the worktree but absent from HEAD means the
|
|
271
|
+
// pending change already carries a hand-authored record for this governance
|
|
272
|
+
// ID — the auto-injected subject-derived item would duplicate it at lower
|
|
273
|
+
// specificity (observed 2026-10-08: 27 files carried descriptive + generic
|
|
274
|
+
// RFC-1237 items). HEAD-resident same-ID items do NOT suppress: consecutive
|
|
275
|
+
// commits under one RFC legitimately stack step items.
|
|
276
|
+
if (headSource !== undefined) {
|
|
277
|
+
const headBlock = headSource.match(CHANGE_SUMMARY_BLOCK_RE);
|
|
278
|
+
const headItems = headBlock ? parseChangeSummary(headBlock[0]).items : [];
|
|
279
|
+
const idInWorktree = items.some((item) => leadingGovernanceId(item, policy) === id);
|
|
280
|
+
const idInHead = headItems.some((item) => leadingGovernanceId(item, policy) === id);
|
|
281
|
+
if (idInWorktree && !idInHead) {
|
|
282
|
+
return { recorded: false, collapsed: false, skipReason: "id-in-diff" };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
231
286
|
const nextItems = [...items, newItem];
|
|
232
287
|
const collapsedIds: string[] = [];
|
|
233
288
|
while (nextItems.length > CHANGE_SUMMARY_WINDOW) {
|
|
@@ -246,6 +301,27 @@ export async function recordSummaryItem(
|
|
|
246
301
|
return { recorded: true, collapsed };
|
|
247
302
|
}
|
|
248
303
|
|
|
304
|
+
function execGit(cwd: string, args: string[]): Promise<string | null> {
|
|
305
|
+
return new Promise((resolve) => {
|
|
306
|
+
execFile("git", args, { cwd, timeout: 5000 }, (err, stdout) => {
|
|
307
|
+
resolve(err ? null : stdout);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Read `path`'s content at HEAD — the baseline for the id-in-diff dedup.
|
|
314
|
+
* Returns undefined when the file is untracked or the workspace is not a git
|
|
315
|
+
* repo (the dedup then falls back to exact-text matching only).
|
|
316
|
+
*/
|
|
317
|
+
async function readHeadSource(gitTop: string | null, absPath: string): Promise<string | undefined> {
|
|
318
|
+
if (!gitTop) return undefined;
|
|
319
|
+
const rel = relative(gitTop, absPath).replace(/\\/g, "/");
|
|
320
|
+
if (rel === "" || rel.startsWith("..")) return undefined;
|
|
321
|
+
const source = await execGit(gitTop, ["show", `HEAD:${rel}`]);
|
|
322
|
+
return source ?? undefined;
|
|
323
|
+
}
|
|
324
|
+
|
|
249
325
|
function riskReminderNeeded(relPath: string, source: string, policy: CompassPolicy): boolean {
|
|
250
326
|
const segments = relPath.split("/").filter(Boolean);
|
|
251
327
|
const workspaceRel = getWorkspaceRelativeSegments(segments, policy).join("/");
|
|
@@ -278,7 +354,10 @@ export async function runCompassSummaryRecord(
|
|
|
278
354
|
: [];
|
|
279
355
|
|
|
280
356
|
const rawText = input.flags["text"];
|
|
281
|
-
|
|
357
|
+
// RFC-1233: strip the git-trailer block at the single injection point —
|
|
358
|
+
// ecosystem.commit and mission.git.commit both pipe the raw message here.
|
|
359
|
+
const stripped = typeof rawText === "string" ? stripGitTrailers(rawText).trim() : "";
|
|
360
|
+
const text = stripped.length > 0 ? stripped : id;
|
|
282
361
|
|
|
283
362
|
const result: SummaryRecordResult = {
|
|
284
363
|
command: "compass.summary.record",
|
|
@@ -289,6 +368,9 @@ export async function runCompassSummaryRecord(
|
|
|
289
368
|
keyDecisionsReminders: [],
|
|
290
369
|
};
|
|
291
370
|
|
|
371
|
+
const gitTopRaw = await execGit(baseRoot, ["rev-parse", "--show-toplevel"]);
|
|
372
|
+
const gitTop = gitTopRaw?.trim() || null;
|
|
373
|
+
|
|
292
374
|
for (const file of files) {
|
|
293
375
|
const absPath = resolve(baseRoot, file);
|
|
294
376
|
const relPath = relative(baseRoot, absPath).replace(/\\/g, "/");
|
|
@@ -298,7 +380,16 @@ export async function runCompassSummaryRecord(
|
|
|
298
380
|
continue;
|
|
299
381
|
}
|
|
300
382
|
try {
|
|
301
|
-
const
|
|
383
|
+
const headSource = await readHeadSource(gitTop, absPath);
|
|
384
|
+
const outcome = await recordSummaryItem(
|
|
385
|
+
absPath,
|
|
386
|
+
relPath,
|
|
387
|
+
id,
|
|
388
|
+
text,
|
|
389
|
+
context.dryRun,
|
|
390
|
+
policy,
|
|
391
|
+
headSource,
|
|
392
|
+
);
|
|
302
393
|
if (outcome.skipReason) {
|
|
303
394
|
result.skipped.push({ file, reason: outcome.skipReason });
|
|
304
395
|
continue;
|
|
@@ -16,6 +16,7 @@ import { execFile } from "node:child_process";
|
|
|
16
16
|
import { promisify } from "node:util";
|
|
17
17
|
import { parse as yamlParse } from "yaml";
|
|
18
18
|
import { runCompassAuditRecord } from "../compass-audit-handler.ts";
|
|
19
|
+
import { ambientIo } from "../../../../src/utils/io.ts";
|
|
19
20
|
import type { ForgeCommandInput, ForgeRuntimeContext } from "../../../../src/types.ts";
|
|
20
21
|
|
|
21
22
|
const execFileAsync = promisify(execFile);
|
|
@@ -42,7 +43,7 @@ function makeContext(workspaceRoot: string): ForgeRuntimeContext {
|
|
|
42
43
|
logger: logger as never,
|
|
43
44
|
dryRun: false,
|
|
44
45
|
outputFormat: "json",
|
|
45
|
-
io:
|
|
46
|
+
io: ambientIo,
|
|
46
47
|
actualState: undefined as never,
|
|
47
48
|
fileIntents: [],
|
|
48
49
|
} as unknown as ForgeRuntimeContext;
|