@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,378 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Shared pure helpers for living feature specs — heading parsing, RFC-namespaced
|
|
4
|
+
section mechanics, spec (de)serialization, domain derivation, and the docs.archive
|
|
5
|
+
this-run merge-target selection (RFC-0711, RFC-0957, RFC-1230).</purpose>
|
|
6
|
+
<non-goals>
|
|
7
|
+
<item>Do not implement command handlers — spec.live.merge/validate/rebuild handlers live in their own files.</item>
|
|
8
|
+
<item>Do not touch the filesystem — these helpers are pure; IO stays in handlers.</item>
|
|
9
|
+
</non-goals>
|
|
10
|
+
<CHANGE_SUMMARY>
|
|
11
|
+
<item>RFC-1230: extracted shared helpers from live-spec-merge.ts; added removeNamespacedSections, seedSpecPrefix, and collectLiveMergeTargets for the idempotent-merge + rebuild design.</item>
|
|
12
|
+
<item>RFC-1230: review findings — scoped droppedSections to namespaced headings, warn on unreadable spec, fail-fast merge on corrupt frontmatter, CHANGE_SUMMARY dedupe</item>
|
|
13
|
+
<item>RFC-1232: fence-aware heading mechanics — private fencedLineIndexes (CommonMark-subset ``` / ~~~ map) consumed by parseHeadings, extractDesignSection, namespaceHeadings, removeNamespacedSections, and applyDeltasToSpecBody (regex lookaheads replaced with line-slice boundaries).</item>
|
|
14
|
+
<item>RFC-1235: extracted docs.archive merge-outcome accounting — classifyLiveMergeOutcome, liveMergeFailureEntry, formatLiveMergeFailure, buildLiveMergeBlock — so exit-nonzero merges record to failed[] instead of merged[].</item>
|
|
15
|
+
</CHANGE_SUMMARY>
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import YAML from "yaml";
|
|
20
|
+
import { listRfcFiles } from "../rfc/frontmatter-io.ts";
|
|
21
|
+
import type {
|
|
22
|
+
LivingSpec,
|
|
23
|
+
DeltaOperation,
|
|
24
|
+
} from "./live-spec-types.ts";
|
|
25
|
+
|
|
26
|
+
export const LIVE_SPECS_DIR = "docs/specs/live";
|
|
27
|
+
|
|
28
|
+
export interface ParsedHeading {
|
|
29
|
+
level: number;
|
|
30
|
+
text: string;
|
|
31
|
+
body: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// RFC-1232: CommonMark-subset fence map — line indexes inside fenced code
|
|
35
|
+
// blocks. A run of ` or ~ of length >=3 with <=3 leading spaces opens a fence;
|
|
36
|
+
// the same-or-longer run of the same character closes it. Opener and closer
|
|
37
|
+
// lines count as interior. An unterminated fence makes the rest of the input
|
|
38
|
+
// interior (CommonMark behavior). Indented code blocks are out of scope.
|
|
39
|
+
const FENCE_OPEN_REGEX = /^ {0,3}(`{3,}|~{3,})/;
|
|
40
|
+
// CommonMark: a closing fence carries no info string — only trailing
|
|
41
|
+
// whitespace. A same-char run followed by text is interior content, so the
|
|
42
|
+
// map over-masks rather than exposing a would-be heading (safe direction).
|
|
43
|
+
// CRLF note: `[ \t]*$` deliberately excludes \r, so a CRLF closer never
|
|
44
|
+
// matches and the tail stays interior — over-masking is the safe direction
|
|
45
|
+
// and repo content is LF-normalized.
|
|
46
|
+
const FENCE_CLOSE_REGEX = /^ {0,3}(`{3,}|~{3,})[ \t]*$/;
|
|
47
|
+
function fencedLineIndexes(lines: string[]): ReadonlySet<number> {
|
|
48
|
+
const inside = new Set<number>();
|
|
49
|
+
let fenceChar: "`" | "~" | null = null;
|
|
50
|
+
let fenceLength = 0;
|
|
51
|
+
for (let i = 0; i < lines.length; i++) {
|
|
52
|
+
const match = lines[i]!.match(FENCE_OPEN_REGEX);
|
|
53
|
+
if (fenceChar === null) {
|
|
54
|
+
if (match) {
|
|
55
|
+
fenceChar = match[1]![0] as "`" | "~";
|
|
56
|
+
fenceLength = match[1]!.length;
|
|
57
|
+
inside.add(i);
|
|
58
|
+
}
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
inside.add(i);
|
|
62
|
+
const closeMatch = lines[i]!.match(FENCE_CLOSE_REGEX);
|
|
63
|
+
if (closeMatch && closeMatch[1]![0] === fenceChar && closeMatch[1]!.length >= fenceLength) {
|
|
64
|
+
fenceChar = null;
|
|
65
|
+
fenceLength = 0;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return inside;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function extractDesignSection(rfcBody: string): string {
|
|
72
|
+
const lines = rfcBody.split("\n");
|
|
73
|
+
const fenced = fencedLineIndexes(lines);
|
|
74
|
+
let startLine = -1;
|
|
75
|
+
for (let i = 0; i < lines.length; i++) {
|
|
76
|
+
if (!fenced.has(i) && /^##\s+Design\s*$/.test(lines[i]!)) {
|
|
77
|
+
startLine = i;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (startLine === -1) return "";
|
|
82
|
+
let endLine = lines.length;
|
|
83
|
+
for (let i = startLine + 1; i < lines.length; i++) {
|
|
84
|
+
if (!fenced.has(i) && /^##\s/.test(lines[i]!)) {
|
|
85
|
+
endLine = i;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return lines.slice(startLine + 1, endLine).join("\n").trim();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function parseHeadings(content: string): ParsedHeading[] {
|
|
93
|
+
const lines = content.split("\n");
|
|
94
|
+
const fenced = fencedLineIndexes(lines);
|
|
95
|
+
const headings: ParsedHeading[] = [];
|
|
96
|
+
let currentHeading: ParsedHeading | null = null;
|
|
97
|
+
let currentBody: string[] = [];
|
|
98
|
+
|
|
99
|
+
for (let i = 0; i < lines.length; i++) {
|
|
100
|
+
const line = lines[i]!;
|
|
101
|
+
const headingMatch = fenced.has(i) ? null : line.match(/^(#{3,})\s+(.+)$/);
|
|
102
|
+
if (headingMatch) {
|
|
103
|
+
if (currentHeading) {
|
|
104
|
+
currentHeading.body = currentBody.join("\n").trim();
|
|
105
|
+
headings.push(currentHeading);
|
|
106
|
+
}
|
|
107
|
+
currentHeading = {
|
|
108
|
+
level: headingMatch[1].length,
|
|
109
|
+
text: headingMatch[2].trim(),
|
|
110
|
+
body: "",
|
|
111
|
+
};
|
|
112
|
+
currentBody = [];
|
|
113
|
+
} else if (currentHeading) {
|
|
114
|
+
currentBody.push(line);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (currentHeading) {
|
|
118
|
+
currentHeading.body = currentBody.join("\n").trim();
|
|
119
|
+
headings.push(currentHeading);
|
|
120
|
+
}
|
|
121
|
+
return headings;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function deriveDomain(frontmatter: Record<string, unknown>): string | null {
|
|
125
|
+
const liveSpec = frontmatter["liveSpec"];
|
|
126
|
+
if (typeof liveSpec === "string" && liveSpec.length > 0) {
|
|
127
|
+
return liveSpec;
|
|
128
|
+
}
|
|
129
|
+
if (liveSpec === true) {
|
|
130
|
+
const packagesImpacted = frontmatter["packagesImpacted"];
|
|
131
|
+
if (Array.isArray(packagesImpacted) && packagesImpacted.length > 0) {
|
|
132
|
+
const firstPkg = String(packagesImpacted[0]);
|
|
133
|
+
return firstPkg.replace(/^packages\//, "").replace(/^@[^/]+\//, "");
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function parseLivingSpec(content: string): LivingSpec | null {
|
|
141
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
142
|
+
if (!match) return null;
|
|
143
|
+
const fm = (YAML.parse(match[1]!) ?? {}) as Record<string, unknown>;
|
|
144
|
+
return {
|
|
145
|
+
domain: String(fm["domain"] ?? ""),
|
|
146
|
+
title: String(fm["title"] ?? ""),
|
|
147
|
+
lastMergedRfc: String(fm["lastMergedRfc"] ?? ""),
|
|
148
|
+
updatedAt: String(fm["updatedAt"] ?? ""),
|
|
149
|
+
createdAt: String(fm["createdAt"] ?? ""),
|
|
150
|
+
history: (Array.isArray(fm["history"]) ? fm["history"] : []) as LivingSpec["history"],
|
|
151
|
+
body: match[2] ?? "",
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function serializeLivingSpec(spec: LivingSpec): string {
|
|
156
|
+
const fm: Record<string, unknown> = {
|
|
157
|
+
domain: spec.domain,
|
|
158
|
+
title: spec.title,
|
|
159
|
+
lastMergedRfc: spec.lastMergedRfc,
|
|
160
|
+
updatedAt: spec.updatedAt,
|
|
161
|
+
createdAt: spec.createdAt,
|
|
162
|
+
history: spec.history,
|
|
163
|
+
};
|
|
164
|
+
const fmStr = YAML.stringify(fm).trimEnd();
|
|
165
|
+
return `---\n${fmStr}\n---\n\n${spec.body}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function findHeadingInSpec(spec: LivingSpec, headingText: string): ParsedHeading | null {
|
|
169
|
+
const headings = parseHeadings(spec.body);
|
|
170
|
+
return headings.find((h) => h.text === headingText) ?? null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function applyDeltasToSpecBody(
|
|
174
|
+
specBody: string,
|
|
175
|
+
deltas: ParsedHeading[],
|
|
176
|
+
operations: DeltaOperation[],
|
|
177
|
+
): string {
|
|
178
|
+
const lines = specBody.split("\n");
|
|
179
|
+
for (const delta of deltas) {
|
|
180
|
+
const op = operations.find((o) => o.heading === delta.text);
|
|
181
|
+
if (!op) continue;
|
|
182
|
+
|
|
183
|
+
if (op.type === "added") {
|
|
184
|
+
lines.push("", `${"#".repeat(delta.level)} ${delta.text}`, "", ...delta.body.split("\n"));
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// RFC-1232: line-slice boundaries instead of `(?=#{3,}|$)` regex lookaheads —
|
|
189
|
+
// those were unanchored and matched `###` inside fenced code blocks.
|
|
190
|
+
const fenced = fencedLineIndexes(lines);
|
|
191
|
+
let start = -1;
|
|
192
|
+
let next = lines.length;
|
|
193
|
+
for (let i = 0; i < lines.length; i++) {
|
|
194
|
+
if (fenced.has(i)) continue;
|
|
195
|
+
const m = lines[i]!.match(/^(#{3,})\s+(.+)$/);
|
|
196
|
+
if (!m) continue;
|
|
197
|
+
if (start === -1) {
|
|
198
|
+
if (m[1]!.length === delta.level && m[2]!.trim() === delta.text) start = i;
|
|
199
|
+
} else {
|
|
200
|
+
next = i;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (start === -1) continue;
|
|
205
|
+
|
|
206
|
+
if (op.type === "modified") {
|
|
207
|
+
lines.splice(start + 1, next - start - 1, "", ...delta.body.split("\n"));
|
|
208
|
+
} else {
|
|
209
|
+
// removed — absorb the blank lines separating the heading from the
|
|
210
|
+
// preceding content (old `\n*` regex prefix), then collapse the section
|
|
211
|
+
// to a single empty line.
|
|
212
|
+
let removeFrom = start;
|
|
213
|
+
while (removeFrom > 0 && lines[removeFrom - 1]!.trim() === "") removeFrom--;
|
|
214
|
+
lines.splice(removeFrom, next - removeFrom, "");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return lines.join("\n").trim();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function escapeRegex(str: string): string {
|
|
221
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function namespaceHeadings(content: string, rfcId: string): string {
|
|
225
|
+
const lines = content.split("\n");
|
|
226
|
+
const fenced = fencedLineIndexes(lines);
|
|
227
|
+
return lines
|
|
228
|
+
.map((line, i) => {
|
|
229
|
+
if (fenced.has(i)) return line;
|
|
230
|
+
const m = line.match(/^(#{3,})\s+(.+)$/);
|
|
231
|
+
if (!m) return line;
|
|
232
|
+
// parseHeadings trims heading text — normalize here too so a heading like
|
|
233
|
+
// "### Foo " round-trips to "### Foo (RFC-XXXX)", not "### Foo (RFC-XXXX)".
|
|
234
|
+
const normalized = m[2]!.trim();
|
|
235
|
+
if (/\(RFC-\d{4}\)$/.test(normalized)) return `${m[1]} ${normalized}`;
|
|
236
|
+
return `${m[1]} ${normalized} (${rfcId})`;
|
|
237
|
+
})
|
|
238
|
+
.join("\n");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// RFC-1230: strip every ###+ section whose heading carries the ` (RFC-XXXX)`
|
|
242
|
+
// marker. Line-based and byte-preserving for kept sections — parseHeadings is
|
|
243
|
+
// lossy on whitespace, so surgical replacement filters lines instead.
|
|
244
|
+
export function removeNamespacedSections(body: string, rfcId: string): string {
|
|
245
|
+
const suffix = ` (${rfcId})`;
|
|
246
|
+
const lines = body.split("\n");
|
|
247
|
+
const fenced = fencedLineIndexes(lines);
|
|
248
|
+
const out: string[] = [];
|
|
249
|
+
let skipping = false;
|
|
250
|
+
for (let i = 0; i < lines.length; i++) {
|
|
251
|
+
const line = lines[i]!;
|
|
252
|
+
const headingMatch = fenced.has(i) ? null : line.match(/^(#{3,})\s+(.+)$/);
|
|
253
|
+
if (headingMatch) {
|
|
254
|
+
skipping = headingMatch[2]!.trim().endsWith(suffix);
|
|
255
|
+
}
|
|
256
|
+
if (!skipping) out.push(line);
|
|
257
|
+
}
|
|
258
|
+
return out.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function findRfcFile(rfcDir: string, rfcId: string): Promise<string | null> {
|
|
262
|
+
return (async () => {
|
|
263
|
+
const files = await listRfcFiles(rfcDir);
|
|
264
|
+
for (const file of files) {
|
|
265
|
+
const basename = path.basename(file);
|
|
266
|
+
if (basename.toLowerCase().startsWith(rfcId.toLowerCase())) {
|
|
267
|
+
return file;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
271
|
+
})();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Canonical spec seed — identical to what the spec.live.merge creation path
|
|
275
|
+
// emits, so rebuild and first-merge produce the same skeleton.
|
|
276
|
+
export function seedSpecPrefix(domain: string, generatedHeader: string): string {
|
|
277
|
+
return `${generatedHeader}# Living Spec: ${domain}\n\n## Overview\n\n`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export interface LiveMergeTargetSelection {
|
|
281
|
+
// RFCs moved into archive/implemented/ this run — merge candidates (still
|
|
282
|
+
// gated on a liveSpec frontmatter check by the caller).
|
|
283
|
+
candidates: string[];
|
|
284
|
+
// RFCs moved into archive/rejected/ this run — counted as skipped when they
|
|
285
|
+
// carry liveSpec (a rejected RFC must never merge).
|
|
286
|
+
rejected: string[];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// RFC-1230: the docs.archive post-loop merges only RFCs moved this run — the
|
|
290
|
+
// selection derives from rfc.archive's `moved` result, not a rescan of the
|
|
291
|
+
// whole docs/rfcs tree (which is what caused the duplicate-merge corruption).
|
|
292
|
+
export function collectLiveMergeTargets(
|
|
293
|
+
moved: ReadonlyArray<{ id: string; status: string; direction: string }>,
|
|
294
|
+
): LiveMergeTargetSelection {
|
|
295
|
+
const candidates: string[] = [];
|
|
296
|
+
const rejected: string[] = [];
|
|
297
|
+
for (const m of moved) {
|
|
298
|
+
if (m.direction !== "into-archive") continue;
|
|
299
|
+
if (m.status === "implemented") candidates.push(m.id);
|
|
300
|
+
else if (m.status === "rejected") rejected.push(m.id);
|
|
301
|
+
}
|
|
302
|
+
return { candidates, rejected };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ── RFC-1235: docs.archive post-loop merge-outcome accounting ────────────────
|
|
306
|
+
// spec.live.merge can return exitCode !== 0 with a populated data object
|
|
307
|
+
// (RFC-1230 fail-fast on corrupt spec frontmatter) — the post-loop must record
|
|
308
|
+
// those distinctly instead of inflating merged[]. Helper-local shapes only;
|
|
309
|
+
// spec.live.merge's own result contract is untouched.
|
|
310
|
+
|
|
311
|
+
export interface LiveMergeEntry {
|
|
312
|
+
id: string;
|
|
313
|
+
domain: string;
|
|
314
|
+
operation: string;
|
|
315
|
+
conflicts: number;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export interface LiveMergeFailure {
|
|
319
|
+
id: string;
|
|
320
|
+
exitCode?: number;
|
|
321
|
+
error: string;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export type LiveMergeOutcome =
|
|
325
|
+
| { kind: "merged"; entry: LiveMergeEntry }
|
|
326
|
+
| { kind: "failed"; entry: LiveMergeFailure };
|
|
327
|
+
|
|
328
|
+
export function classifyLiveMergeOutcome(
|
|
329
|
+
rfcId: string,
|
|
330
|
+
result: { exitCode?: number; data?: unknown; summary?: string },
|
|
331
|
+
): LiveMergeOutcome {
|
|
332
|
+
const data = result.data as
|
|
333
|
+
| { domain?: unknown; operation?: unknown; conflicts?: unknown }
|
|
334
|
+
| undefined;
|
|
335
|
+
if (result.exitCode === 0 && data) {
|
|
336
|
+
return {
|
|
337
|
+
kind: "merged",
|
|
338
|
+
entry: {
|
|
339
|
+
id: rfcId,
|
|
340
|
+
domain: String(data.domain ?? ""),
|
|
341
|
+
operation: String(data.operation ?? ""),
|
|
342
|
+
conflicts: Array.isArray(data.conflicts) ? data.conflicts.length : 0,
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
kind: "failed",
|
|
348
|
+
entry: {
|
|
349
|
+
id: rfcId,
|
|
350
|
+
exitCode: result.exitCode,
|
|
351
|
+
error:
|
|
352
|
+
result.summary ??
|
|
353
|
+
(result.exitCode === undefined
|
|
354
|
+
? "spec.live.merge returned no exit code"
|
|
355
|
+
: `spec.live.merge exited ${result.exitCode}`),
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function liveMergeFailureEntry(rfcId: string, error: unknown): LiveMergeFailure {
|
|
361
|
+
return { id: rfcId, error: String(error instanceof Error ? error.message : error) };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function formatLiveMergeFailure(entry: LiveMergeFailure): string {
|
|
365
|
+
return `spec.live.merge: failed for ${entry.id}: ${entry.error}`;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Emission gate + block assembly for results["spec.live.merge"] — undefined
|
|
369
|
+
* when nothing was attempted, so an all-failed run still surfaces. */
|
|
370
|
+
export function buildLiveMergeBlock(
|
|
371
|
+
merged: LiveMergeEntry[],
|
|
372
|
+
failed: LiveMergeFailure[],
|
|
373
|
+
skipped: number,
|
|
374
|
+
dryRun: boolean,
|
|
375
|
+
): { merged: LiveMergeEntry[]; failed: LiveMergeFailure[]; skipped: number; dryRun: boolean } | undefined {
|
|
376
|
+
if (merged.length === 0 && failed.length === 0 && skipped === 0) return undefined;
|
|
377
|
+
return { merged, failed, skipped, dryRun };
|
|
378
|
+
}
|
|
@@ -8,6 +8,9 @@ merge results, and validation (RFC-0711).</purpose>
|
|
|
8
8
|
</MODULE_CONTRACT>
|
|
9
9
|
<CHANGE_SUMMARY>
|
|
10
10
|
<item>RFC-0711: initial living spec types — LivingSpec, DeltaOperation, DeltaConflict, merge/list/show/validate result types.</item>
|
|
11
|
+
<item>RFC-1230: added already-merged to SpecLiveMergeResult.operation; added SpecLiveRebuildResult for spec.live.rebuild.</item>
|
|
12
|
+
<item>RFC-1230: review findings — scoped droppedSections to namespaced headings, warn on unreadable spec, fail-fast merge on corrupt frontmatter, CHANGE_SUMMARY dedupe</item>
|
|
13
|
+
<item>RFC-1234: LivingSpecViolation gains optional severity field (absent = error; errors drive exit code); uniqueRfcs contract comment states deduplicated-history semantics.</item>
|
|
11
14
|
</CHANGE_SUMMARY>
|
|
12
15
|
*/
|
|
13
16
|
|
|
@@ -47,12 +50,26 @@ export interface DeltaConflict {
|
|
|
47
50
|
export interface SpecLiveMergeResult {
|
|
48
51
|
command: "spec.live.merge";
|
|
49
52
|
domain: string;
|
|
50
|
-
operation: "created" | "modified";
|
|
53
|
+
operation: "created" | "modified" | "already-merged";
|
|
51
54
|
deltas: DeltaOperation[];
|
|
52
55
|
conflicts: DeltaConflict[];
|
|
53
56
|
dryRun: boolean;
|
|
54
57
|
}
|
|
55
58
|
|
|
59
|
+
export interface SpecLiveRebuildResult {
|
|
60
|
+
command: "spec.live.rebuild";
|
|
61
|
+
domain: string;
|
|
62
|
+
operation: "rebuilt" | "unchanged" | "skipped";
|
|
63
|
+
/** Unique RFC ids present in the deduplicated history — unreadable or
|
|
64
|
+
* not-implemented ids are still counted; the replayed count is
|
|
65
|
+
* uniqueRfcs − unreadableRfcs.length. */
|
|
66
|
+
uniqueRfcs: number;
|
|
67
|
+
droppedHistoryEntries: number;
|
|
68
|
+
droppedSections: number;
|
|
69
|
+
unreadableRfcs: string[];
|
|
70
|
+
dryRun: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
56
73
|
export interface SpecLiveListEntry {
|
|
57
74
|
domain: string;
|
|
58
75
|
title: string;
|
|
@@ -83,6 +100,9 @@ export interface LivingSpecViolation {
|
|
|
83
100
|
rule: string;
|
|
84
101
|
message: string;
|
|
85
102
|
domain?: string;
|
|
103
|
+
/** Absent === "error"; only error-severity violations drive a non-zero
|
|
104
|
+
* exit code (RFC-1234). */
|
|
105
|
+
severity?: "error" | "warning";
|
|
86
106
|
}
|
|
87
107
|
|
|
88
108
|
export interface SpecLiveValidateResult {
|