kibi-cli 0.10.0 → 0.10.1

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.
Files changed (42) hide show
  1. package/dist/cli.js +2 -0
  2. package/dist/commands/check.d.ts.map +1 -1
  3. package/dist/commands/check.js +204 -3
  4. package/dist/commands/init-helpers.d.ts.map +1 -1
  5. package/dist/commands/init-helpers.js +11 -14
  6. package/dist/commands/sync/manifest.d.ts +8 -2
  7. package/dist/commands/sync/manifest.d.ts.map +1 -1
  8. package/dist/commands/sync/manifest.js +56 -11
  9. package/dist/commands/sync.d.ts +1 -0
  10. package/dist/commands/sync.d.ts.map +1 -1
  11. package/dist/commands/sync.js +9 -7
  12. package/dist/extractors/manifest.d.ts +30 -0
  13. package/dist/extractors/manifest.d.ts.map +1 -1
  14. package/dist/extractors/manifest.js +60 -7
  15. package/dist/extractors/symbol-coordinates.d.ts +15 -0
  16. package/dist/extractors/symbol-coordinates.d.ts.map +1 -0
  17. package/dist/extractors/symbol-coordinates.js +83 -0
  18. package/dist/public/extractors/manifest.d.ts +1 -1
  19. package/dist/public/extractors/manifest.d.ts.map +1 -1
  20. package/dist/public/extractors/manifest.js +1 -1
  21. package/dist/traceability/evidence-model.d.ts +142 -0
  22. package/dist/traceability/evidence-model.d.ts.map +1 -0
  23. package/dist/traceability/evidence-model.js +70 -0
  24. package/dist/traceability/git-staged.d.ts +1 -0
  25. package/dist/traceability/git-staged.d.ts.map +1 -1
  26. package/dist/traceability/git-staged.js +28 -3
  27. package/dist/traceability/staged-diagnostics.d.ts +25 -0
  28. package/dist/traceability/staged-diagnostics.d.ts.map +1 -0
  29. package/dist/traceability/staged-diagnostics.js +67 -0
  30. package/dist/traceability/staged-impact-contract.d.ts +57 -0
  31. package/dist/traceability/staged-impact-contract.d.ts.map +1 -0
  32. package/dist/traceability/staged-impact-contract.js +202 -0
  33. package/dist/traceability/staged-symbols-manifest.d.ts +23 -0
  34. package/dist/traceability/staged-symbols-manifest.d.ts.map +1 -0
  35. package/dist/traceability/staged-symbols-manifest.js +269 -0
  36. package/dist/traceability/symbol-extract.d.ts.map +1 -1
  37. package/dist/traceability/symbol-extract.js +33 -9
  38. package/dist/utils/manifest-paths.d.ts +8 -0
  39. package/dist/utils/manifest-paths.d.ts.map +1 -0
  40. package/dist/utils/manifest-paths.js +62 -0
  41. package/package.json +1 -1
  42. package/src/public/extractors/manifest.ts +2 -0
@@ -0,0 +1,57 @@
1
+ export declare const KIBI_IMPACT_DIAGNOSTIC_IDS: readonly ["kibi_impact_evidence_missing", "symbols_manifest_stale", "kibi_impact_override_missing_rationale"];
2
+ export type KibiImpactDiagnosticId = (typeof KIBI_IMPACT_DIAGNOSTIC_IDS)[number];
3
+ export type KibiImpactEvidenceKind = "entity_markdown" | "symbols_manifest" | "audited_no_impact";
4
+ export interface KibiImpactDiagnosticContract {
5
+ id: KibiImpactDiagnosticId;
6
+ title: string;
7
+ resolution: [string, string, string];
8
+ }
9
+ export interface KibiImpactEvidenceInput {
10
+ filePath?: string;
11
+ extractionOutputChanged?: boolean;
12
+ overrideDeclared?: boolean;
13
+ overrideRationale?: string | null;
14
+ symbolsManifestPath?: string;
15
+ }
16
+ export interface BehaviorSourceEditInput {
17
+ path: string;
18
+ diffText: string;
19
+ intersectsBehaviorBearingSymbol: boolean;
20
+ knownUserFacingSurface?: boolean;
21
+ }
22
+ export interface ParsedKibiImpactOverride {
23
+ declared: boolean;
24
+ rationale: string | null;
25
+ }
26
+ export interface AuditedNoImpactOverrideInput {
27
+ behaviorSourceEdit: boolean;
28
+ override: ParsedKibiImpactOverride;
29
+ }
30
+ /**
31
+ * Staged Kibi impact contract for `kibi check --staged`.
32
+ *
33
+ * This is intentionally a small, stable contract for tests and future staged
34
+ * enforcement wiring:
35
+ * - `behavior_source_edit` is a supported source-file edit whose staged hunks
36
+ * intersect exported or other behavior-bearing/user-facing surfaces and whose
37
+ * changed lines are not comment-only or formatting-only.
38
+ * - `kibi_impact_evidence` is staged entity markdown, staged authored
39
+ * `documentation/symbols.yaml` metadata, refreshed
40
+ * `documentation/symbol-coordinates.yaml` when extraction output changes, or
41
+ * an explicit audited `Kibi-Impact: none` declaration with
42
+ * rationale for false positives/non-behavior-only edits.
43
+ * - `Kibi-Impact: none` never satisfies a genuine behavior change.
44
+ * - Test-only edits do not require new KB evidence unless they introduce
45
+ * executable test symbols that need traceability.
46
+ *
47
+ * This contract deliberately avoids broad semantic diffing and stays
48
+ * conservative so later staged-check enforcement can share exact diagnostic IDs
49
+ * and operator-facing remediation steps.
50
+ */
51
+ export declare const KIBI_IMPACT_DIAGNOSTICS: Record<KibiImpactDiagnosticId, KibiImpactDiagnosticContract>;
52
+ export declare function classifyKibiImpactEvidence(input: KibiImpactEvidenceInput): KibiImpactEvidenceKind | null;
53
+ export declare function parseKibiImpactOverride(text: string): ParsedKibiImpactOverride;
54
+ export declare function isAuditedNoImpactOverrideAllowed(input: AuditedNoImpactOverrideInput): boolean;
55
+ export declare function isBehaviorSourceEdit(input: BehaviorSourceEditInput): boolean;
56
+ export declare function isSupportedBehaviorSourcePath(filePath: string): boolean;
57
+ //# sourceMappingURL=staged-impact-contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"staged-impact-contract.d.ts","sourceRoot":"","sources":["../../src/traceability/staged-impact-contract.ts"],"names":[],"mappings":"AAqBA,eAAO,MAAM,0BAA0B,+GAI7B,CAAC;AAEX,MAAM,MAAM,sBAAsB,GAChC,CAAC,OAAO,0BAA0B,CAAC,CAAC,MAAM,CAAC,CAAC;AAE9C,MAAM,MAAM,sBAAsB,GAC9B,iBAAiB,GACjB,kBAAkB,GAClB,mBAAmB,CAAC;AAExB,MAAM,WAAW,4BAA4B;IAC3C,EAAE,EAAE,sBAAsB,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,+BAA+B,EAAE,OAAO,CAAC;IACzC,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,4BAA4B;IAC3C,kBAAkB,EAAE,OAAO,CAAC;IAC5B,QAAQ,EAAE,wBAAwB,CAAC;CACpC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,uBAAuB,EAAE,MAAM,CAC1C,sBAAsB,EACtB,4BAA4B,CA6B7B,CAAC;AAEF,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,uBAAuB,GAC7B,sBAAsB,GAAG,IAAI,CAsB/B;AAED,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,MAAM,GACX,wBAAwB,CAS1B;AAED,wBAAgB,gCAAgC,CAC9C,KAAK,EAAE,4BAA4B,GAClC,OAAO,CAMT;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,uBAAuB,GAAG,OAAO,CA8B5E;AAED,wBAAgB,6BAA6B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAQvE"}
@@ -0,0 +1,202 @@
1
+ const SUPPORTED_BEHAVIOR_SOURCE_EXTENSIONS = new Set([
2
+ ".ts",
3
+ ".tsx",
4
+ ".js",
5
+ ".jsx",
6
+ ".mts",
7
+ ".cts",
8
+ ".mjs",
9
+ ".cjs",
10
+ ]);
11
+ const ENTITY_EVIDENCE_SEGMENTS = [
12
+ "/requirements/",
13
+ "/scenarios/",
14
+ "/tests/",
15
+ "/facts/",
16
+ "/adr/",
17
+ "/flags/",
18
+ "/events/",
19
+ ];
20
+ export const KIBI_IMPACT_DIAGNOSTIC_IDS = [
21
+ "kibi_impact_evidence_missing",
22
+ "symbols_manifest_stale",
23
+ "kibi_impact_override_missing_rationale",
24
+ ];
25
+ /**
26
+ * Staged Kibi impact contract for `kibi check --staged`.
27
+ *
28
+ * This is intentionally a small, stable contract for tests and future staged
29
+ * enforcement wiring:
30
+ * - `behavior_source_edit` is a supported source-file edit whose staged hunks
31
+ * intersect exported or other behavior-bearing/user-facing surfaces and whose
32
+ * changed lines are not comment-only or formatting-only.
33
+ * - `kibi_impact_evidence` is staged entity markdown, staged authored
34
+ * `documentation/symbols.yaml` metadata, refreshed
35
+ * `documentation/symbol-coordinates.yaml` when extraction output changes, or
36
+ * an explicit audited `Kibi-Impact: none` declaration with
37
+ * rationale for false positives/non-behavior-only edits.
38
+ * - `Kibi-Impact: none` never satisfies a genuine behavior change.
39
+ * - Test-only edits do not require new KB evidence unless they introduce
40
+ * executable test symbols that need traceability.
41
+ *
42
+ * This contract deliberately avoids broad semantic diffing and stays
43
+ * conservative so later staged-check enforcement can share exact diagnostic IDs
44
+ * and operator-facing remediation steps.
45
+ */
46
+ export const KIBI_IMPACT_DIAGNOSTICS = {
47
+ kibi_impact_evidence_missing: {
48
+ id: "kibi_impact_evidence_missing",
49
+ title: "Behavior edit requires staged Kibi impact evidence",
50
+ resolution: [
51
+ "Query Kibi via MCP first: use kb_search for discovery, then kb_query for exact follow-up.",
52
+ "Stage related KB entity markdown, stage authored documentation/symbols.yaml metadata, or refresh coordinates with kibi sync --refresh-symbol-coordinates and stage documentation/symbol-coordinates.yaml documentation/symbols.yaml.",
53
+ "Re-run or let the hook run kibi check --staged.",
54
+ ],
55
+ },
56
+ symbols_manifest_stale: {
57
+ id: "symbols_manifest_stale",
58
+ title: "Symbol coordinates evidence is stale for changed extraction output",
59
+ resolution: [
60
+ "Refresh symbol coordinates for the changed source file with kibi sync --refresh-symbol-coordinates.",
61
+ "Stage documentation/symbol-coordinates.yaml in the same change as the behavior edit, and stage documentation/symbols.yaml only if migration cleanup changed it.",
62
+ "Re-run or let the hook run kibi check --staged.",
63
+ ],
64
+ },
65
+ kibi_impact_override_missing_rationale: {
66
+ id: "kibi_impact_override_missing_rationale",
67
+ title: "Kibi no-impact override requires an explicit rationale",
68
+ resolution: [
69
+ "Use an audited declaration only for false positives or non-behavioral edits.",
70
+ "Include both 'Kibi-Impact: none' and a nearby 'Rationale:' line describing why the edit has no behavior impact.",
71
+ "Re-run or let the hook run kibi check --staged.",
72
+ ],
73
+ },
74
+ };
75
+ export function classifyKibiImpactEvidence(input) {
76
+ if (input.overrideDeclared && hasText(input.overrideRationale)) {
77
+ return "audited_no_impact";
78
+ }
79
+ const filePath = input.filePath ?? "";
80
+ if (!filePath) {
81
+ return null;
82
+ }
83
+ if (isEntityEvidenceMarkdown(filePath)) {
84
+ return "entity_markdown";
85
+ }
86
+ if (isSymbolsManifest(filePath, input.symbolsManifestPath) &&
87
+ input.extractionOutputChanged) {
88
+ return "symbols_manifest";
89
+ }
90
+ return null;
91
+ }
92
+ export function parseKibiImpactOverride(text) {
93
+ const declared = /^Kibi-Impact:\s*none\s*$/im.test(text);
94
+ const rationaleMatch = text.match(/^Rationale:\s*(.+)\s*$/im);
95
+ return {
96
+ declared,
97
+ rationale: hasText(rationaleMatch?.[1])
98
+ ? (rationaleMatch?.[1]?.trim() ?? null)
99
+ : null,
100
+ };
101
+ }
102
+ export function isAuditedNoImpactOverrideAllowed(input) {
103
+ return (input.override.declared &&
104
+ hasText(input.override.rationale) &&
105
+ !input.behaviorSourceEdit);
106
+ }
107
+ export function isBehaviorSourceEdit(input) {
108
+ if (!isSupportedBehaviorSourcePath(input.path)) {
109
+ return false;
110
+ }
111
+ if (!input.intersectsBehaviorBearingSymbol && !input.knownUserFacingSurface) {
112
+ return false;
113
+ }
114
+ const changes = extractChangedLines(input.diffText);
115
+ if (changes.length === 0) {
116
+ return false;
117
+ }
118
+ if (changes.every((line) => isIgnorableChangeLine(line))) {
119
+ return false;
120
+ }
121
+ const removed = normalizeChangedLines(changes.filter((line) => line.kind === "remove"));
122
+ const added = normalizeChangedLines(changes.filter((line) => line.kind === "add"));
123
+ if (removed.length > 0 && removed.join("\n") === added.join("\n")) {
124
+ return false;
125
+ }
126
+ return true;
127
+ }
128
+ export function isSupportedBehaviorSourcePath(filePath) {
129
+ for (const extension of SUPPORTED_BEHAVIOR_SOURCE_EXTENSIONS) {
130
+ if (filePath.endsWith(extension)) {
131
+ return true;
132
+ }
133
+ }
134
+ return false;
135
+ }
136
+ function isEntityEvidenceMarkdown(filePath) {
137
+ if (!filePath.endsWith(".md")) {
138
+ return false;
139
+ }
140
+ return ENTITY_EVIDENCE_SEGMENTS.some((segment) => filePath.includes(segment));
141
+ }
142
+ function isSymbolsManifest(filePath, symbolsManifestPath) {
143
+ const candidates = new Set([
144
+ "symbols.yaml",
145
+ "symbols.yml",
146
+ "documentation/symbols.yaml",
147
+ "documentation/symbols.yml",
148
+ ]);
149
+ if (symbolsManifestPath) {
150
+ candidates.add(symbolsManifestPath);
151
+ if (symbolsManifestPath.endsWith(".yaml")) {
152
+ candidates.add(`${symbolsManifestPath.slice(0, -5)}.yml`);
153
+ }
154
+ if (symbolsManifestPath.endsWith(".yml")) {
155
+ candidates.add(`${symbolsManifestPath.slice(0, -4)}.yaml`);
156
+ }
157
+ }
158
+ if (candidates.has(filePath)) {
159
+ return true;
160
+ }
161
+ return (filePath.endsWith("/symbols.yaml") ||
162
+ filePath.endsWith("/symbols.yml") ||
163
+ filePath === "symbols.yaml" ||
164
+ filePath === "symbols.yml");
165
+ }
166
+ function hasText(value) {
167
+ return typeof value === "string" && value.trim().length > 0;
168
+ }
169
+ function extractChangedLines(diffText) {
170
+ const lines = diffText.split(/\r?\n/);
171
+ const changes = [];
172
+ for (const line of lines) {
173
+ if (line.startsWith("+++") ||
174
+ line.startsWith("---") ||
175
+ line.startsWith("@@")) {
176
+ continue;
177
+ }
178
+ if (line.startsWith("+")) {
179
+ changes.push({ kind: "add", text: line.slice(1) });
180
+ continue;
181
+ }
182
+ if (line.startsWith("-")) {
183
+ changes.push({ kind: "remove", text: line.slice(1) });
184
+ }
185
+ }
186
+ return changes;
187
+ }
188
+ function isIgnorableChangeLine(line) {
189
+ const trimmed = line.text.trim();
190
+ if (!trimmed) {
191
+ return true;
192
+ }
193
+ return (trimmed.startsWith("//") ||
194
+ trimmed.startsWith("/*") ||
195
+ trimmed.startsWith("*/") ||
196
+ trimmed.startsWith("*"));
197
+ }
198
+ function normalizeChangedLines(lines) {
199
+ return lines
200
+ .map((line) => line.text.replace(/\s+/g, ""))
201
+ .filter((line) => line.length > 0);
202
+ }
@@ -0,0 +1,23 @@
1
+ import type { StagedFile } from "./git-staged.js";
2
+ export interface StagedSymbolsManifestAssessment {
3
+ state: "fresh" | "stale" | "missing" | "not_required";
4
+ sourcePaths: string[];
5
+ path: string;
6
+ }
7
+ export interface StagedAuthoredSymbolsManifestEvidence {
8
+ path: string;
9
+ entries: Array<{
10
+ sourcePath: string;
11
+ entityIds: string[];
12
+ }>;
13
+ }
14
+ export declare function assessStagedSymbolsManifest(options: {
15
+ symbolsManifestPath: string;
16
+ sourceFiles: StagedFile[];
17
+ stagedFiles: StagedFile[];
18
+ }): StagedSymbolsManifestAssessment;
19
+ export declare function collectStagedAuthoredSymbolsManifestEvidence(options: {
20
+ sourceFiles: StagedFile[];
21
+ stagedFiles: StagedFile[];
22
+ }): StagedAuthoredSymbolsManifestEvidence;
23
+ //# sourceMappingURL=staged-symbols-manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"staged-symbols-manifest.d.ts","sourceRoot":"","sources":["../../src/traceability/staged-symbols-manifest.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AA8BlD,MAAM,WAAW,+BAA+B;IAC9C,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,SAAS,GAAG,cAAc,CAAC;IACtD,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,qCAAqC;IACpD,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,KAAK,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;CAC7D;AAmQD,wBAAgB,2BAA2B,CAAC,OAAO,EAAE;IACnD,mBAAmB,EAAE,MAAM,CAAC;IAC5B,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,WAAW,EAAE,UAAU,EAAE,CAAC;CAC3B,GAAG,+BAA+B,CA0ElC;AAoBD,wBAAgB,4CAA4C,CAAC,OAAO,EAAE;IACpE,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,WAAW,EAAE,UAAU,EAAE,CAAC;CAC3B,GAAG,qCAAqC,CA6CxC"}
@@ -0,0 +1,269 @@
1
+ import { execSync } from "node:child_process";
2
+ import * as path from "node:path";
3
+ import { extractManifestSymbolRecordsString, } from "../extractors/manifest.js";
4
+ import { mergeCoordinatesWithManifest, readCoordinateArtifact, } from "../extractors/symbol-coordinates.js";
5
+ import { analyzeSourceText } from "../extractors/symbols-coordinator.js";
6
+ import { resolveSymbolsManifestPaths } from "../utils/manifest-paths.js";
7
+ function toRepoRelativePath(absoluteOrRelativePath) {
8
+ if (!path.isAbsolute(absoluteOrRelativePath)) {
9
+ return absoluteOrRelativePath.replace(/\\/g, "/");
10
+ }
11
+ const relativePath = path.relative(process.cwd(), absoluteOrRelativePath);
12
+ return relativePath.replace(/\\/g, "/");
13
+ }
14
+ function resolveRelativeManifestPaths(symbolsManifestPath) {
15
+ if (symbolsManifestPath) {
16
+ const normalizedSymbolsPath = toRepoRelativePath(symbolsManifestPath);
17
+ return {
18
+ symbolsPath: normalizedSymbolsPath,
19
+ coordinatesPath: path
20
+ .join(path.dirname(normalizedSymbolsPath), "symbol-coordinates.yaml")
21
+ .replace(/\\/g, "/"),
22
+ };
23
+ }
24
+ const { coordinatesPath, symbolsPath } = resolveSymbolsManifestPaths(process.cwd());
25
+ return {
26
+ symbolsPath: toRepoRelativePath(symbolsPath),
27
+ coordinatesPath: toRepoRelativePath(coordinatesPath),
28
+ };
29
+ }
30
+ function readHeadFileContent(filePath) {
31
+ try {
32
+ return execSync(`git show HEAD:${filePath}`, {
33
+ encoding: "utf8",
34
+ stdio: ["pipe", "pipe", "pipe"],
35
+ });
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ function parseManifestRecords(content, filePath) {
42
+ if (content === null || content === undefined) {
43
+ return [];
44
+ }
45
+ try {
46
+ return extractManifestSymbolRecordsString(content, filePath);
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ }
52
+ function parseCoordinateArtifact(content) {
53
+ if (content === null || content === undefined) {
54
+ return { coordinates: {} };
55
+ }
56
+ try {
57
+ return readCoordinateArtifact(content);
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
63
+ function normalizeNumber(value) {
64
+ return typeof value === "number" ? value : null;
65
+ }
66
+ function normalizeLinks(links) {
67
+ if (!Array.isArray(links)) {
68
+ return [];
69
+ }
70
+ const normalized = [];
71
+ for (const link of links) {
72
+ if (typeof link === "string") {
73
+ normalized.push(link);
74
+ continue;
75
+ }
76
+ if (link &&
77
+ typeof link === "object" &&
78
+ typeof link.type === "string" &&
79
+ typeof link.target === "string") {
80
+ normalized.push({ type: link.type, target: link.target });
81
+ }
82
+ }
83
+ return normalized.sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
84
+ }
85
+ function normalizeRelationships(relationships) {
86
+ if (!Array.isArray(relationships)) {
87
+ return [];
88
+ }
89
+ return relationships
90
+ .flatMap((relationship) => {
91
+ if (relationship &&
92
+ typeof relationship.type === "string" &&
93
+ typeof relationship.target === "string") {
94
+ return [{ type: relationship.type, target: relationship.target }];
95
+ }
96
+ return [];
97
+ })
98
+ .sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
99
+ }
100
+ function normalizeManifestSymbolsForSourceFile(records, sourceFile) {
101
+ return records
102
+ .filter((record) => {
103
+ const recordSource = typeof record.sourceFile === "string"
104
+ ? record.sourceFile
105
+ : typeof record.source === "string"
106
+ ? record.source
107
+ : null;
108
+ return recordSource === sourceFile && typeof record.title === "string";
109
+ })
110
+ .map((record) => ({
111
+ title: record.title,
112
+ sourceFile,
113
+ sourceLine: normalizeNumber(record.sourceLine),
114
+ sourceColumn: normalizeNumber(record.sourceColumn),
115
+ sourceEndLine: normalizeNumber(record.sourceEndLine),
116
+ sourceEndColumn: normalizeNumber(record.sourceEndColumn),
117
+ }))
118
+ .sort(compareNormalizedSymbols);
119
+ }
120
+ function normalizeAuthoredManifestSymbolsForSourceFile(records, sourceFile) {
121
+ return records
122
+ .filter((record) => {
123
+ const recordSource = typeof record.sourceFile === "string"
124
+ ? record.sourceFile
125
+ : typeof record.source === "string"
126
+ ? record.source
127
+ : null;
128
+ return recordSource === sourceFile && typeof record.title === "string";
129
+ })
130
+ .map((record) => ({
131
+ id: typeof record.id === "string" ? record.id : null,
132
+ title: record.title,
133
+ sourceFile,
134
+ status: typeof record.status === "string" ? record.status : null,
135
+ links: normalizeLinks(record.links),
136
+ relationships: normalizeRelationships(record.relationships),
137
+ tags: Array.isArray(record.tags)
138
+ ? record.tags.filter((tag) => typeof tag === "string").sort()
139
+ : [],
140
+ owner: typeof record.owner === "string" ? record.owner : null,
141
+ priority: typeof record.priority === "string" ? record.priority : null,
142
+ severity: typeof record.severity === "string" ? record.severity : null,
143
+ textRef: typeof record.text_ref === "string" ? record.text_ref : null,
144
+ }))
145
+ .sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
146
+ }
147
+ function normalizeExpectedSymbolsForStagedFile(stagedFile) {
148
+ const analysis = analyzeSourceText(stagedFile.path, stagedFile.content ?? "");
149
+ return analysis.symbols
150
+ .map((symbol) => ({
151
+ title: symbol.name,
152
+ sourceFile: stagedFile.path,
153
+ sourceLine: symbol.startLine,
154
+ sourceColumn: symbol.startColumn,
155
+ sourceEndLine: symbol.endLine,
156
+ sourceEndColumn: symbol.endColumn,
157
+ }))
158
+ .sort(compareNormalizedSymbols);
159
+ }
160
+ function compareNormalizedSymbols(left, right) {
161
+ return JSON.stringify(left).localeCompare(JSON.stringify(right));
162
+ }
163
+ function signaturesEqual(left, right) {
164
+ return JSON.stringify(left) === JSON.stringify(right);
165
+ }
166
+ function uniqueSorted(paths) {
167
+ return Array.from(new Set(paths)).sort();
168
+ }
169
+ function mergeManifestRecordsWithCoordinates(manifestRecords, coordinateArtifact) {
170
+ return mergeCoordinatesWithManifest(manifestRecords ?? [], coordinateArtifact);
171
+ }
172
+ function getEffectiveManifestRecords(options) {
173
+ const { headCoordinateArtifact, headManifestRecords, paths, stagedFiles } = options;
174
+ const stagedManifestFile = stagedFiles.find((file) => file.path === paths.symbolsPath);
175
+ const stagedCoordinatesFile = stagedFiles.find((file) => file.path === paths.coordinatesPath);
176
+ const stagedManifestRecords = stagedManifestFile
177
+ ? parseManifestRecords(stagedManifestFile.content, paths.symbolsPath)
178
+ : headManifestRecords;
179
+ const stagedCoordinateArtifact = stagedCoordinatesFile
180
+ ? parseCoordinateArtifact(stagedCoordinatesFile.content)
181
+ : headCoordinateArtifact;
182
+ return {
183
+ stagedManifestFile,
184
+ stagedCoordinatesFile,
185
+ stagedManifestRecords,
186
+ stagedCoordinateArtifact,
187
+ };
188
+ }
189
+ export function assessStagedSymbolsManifest(options) {
190
+ const { sourceFiles, stagedFiles, symbolsManifestPath } = options;
191
+ const paths = resolveRelativeManifestPaths(symbolsManifestPath);
192
+ const headManifestRecords = parseManifestRecords(readHeadFileContent(paths.symbolsPath), paths.symbolsPath);
193
+ const headCoordinateArtifact = parseCoordinateArtifact(readHeadFileContent(paths.coordinatesPath));
194
+ const { stagedCoordinatesFile, stagedCoordinateArtifact, stagedManifestRecords, } = getEffectiveManifestRecords({
195
+ stagedFiles,
196
+ paths,
197
+ headManifestRecords,
198
+ headCoordinateArtifact,
199
+ });
200
+ const baselineMergedRecords = mergeManifestRecordsWithCoordinates(headManifestRecords, headCoordinateArtifact);
201
+ const stagedMergedRecords = mergeManifestRecordsWithCoordinates(stagedManifestRecords, stagedCoordinateArtifact);
202
+ const requiredRefreshPaths = [];
203
+ const freshPaths = new Set();
204
+ for (const sourceFile of sourceFiles) {
205
+ const expectedSymbols = normalizeExpectedSymbolsForStagedFile(sourceFile);
206
+ const baselineSymbols = normalizeManifestSymbolsForSourceFile(baselineMergedRecords, sourceFile.path);
207
+ if (signaturesEqual(expectedSymbols, baselineSymbols)) {
208
+ continue;
209
+ }
210
+ requiredRefreshPaths.push(sourceFile.path);
211
+ if (!stagedCoordinatesFile || stagedCoordinateArtifact === null) {
212
+ continue;
213
+ }
214
+ const stagedSymbols = normalizeManifestSymbolsForSourceFile(stagedMergedRecords, sourceFile.path);
215
+ if (signaturesEqual(expectedSymbols, stagedSymbols)) {
216
+ freshPaths.add(sourceFile.path);
217
+ }
218
+ }
219
+ const sourcePaths = uniqueSorted(requiredRefreshPaths);
220
+ if (sourcePaths.length === 0) {
221
+ return { state: "not_required", sourcePaths: [], path: paths.coordinatesPath };
222
+ }
223
+ if (sourcePaths.every((sourcePath) => freshPaths.has(sourcePath))) {
224
+ return { state: "fresh", sourcePaths, path: paths.coordinatesPath };
225
+ }
226
+ if (!stagedCoordinatesFile) {
227
+ return { state: "missing", sourcePaths, path: paths.coordinatesPath };
228
+ }
229
+ return { state: "stale", sourcePaths, path: paths.coordinatesPath };
230
+ }
231
+ function getEntityIdsForSourceFile(records, sourceFile) {
232
+ return records
233
+ .filter((record) => {
234
+ const recordSource = typeof record.sourceFile === "string"
235
+ ? record.sourceFile
236
+ : typeof record.source === "string"
237
+ ? record.source
238
+ : null;
239
+ return recordSource === sourceFile && typeof record.id === "string";
240
+ })
241
+ .map((record) => record.id)
242
+ .sort();
243
+ }
244
+ export function collectStagedAuthoredSymbolsManifestEvidence(options) {
245
+ const { sourceFiles, stagedFiles } = options;
246
+ const paths = resolveRelativeManifestPaths();
247
+ const stagedManifestFile = stagedFiles.find((file) => file.path === paths.symbolsPath);
248
+ if (!stagedManifestFile) {
249
+ return { path: paths.symbolsPath, entries: [] };
250
+ }
251
+ const headManifestRecords = parseManifestRecords(readHeadFileContent(paths.symbolsPath), paths.symbolsPath) ?? [];
252
+ const stagedManifestRecords = parseManifestRecords(stagedManifestFile.content, paths.symbolsPath);
253
+ if (stagedManifestRecords === null) {
254
+ return { path: paths.symbolsPath, entries: [] };
255
+ }
256
+ const entries = [];
257
+ for (const sourceFile of sourceFiles) {
258
+ const headSymbols = normalizeAuthoredManifestSymbolsForSourceFile(headManifestRecords, sourceFile.path);
259
+ const stagedSymbols = normalizeAuthoredManifestSymbolsForSourceFile(stagedManifestRecords, sourceFile.path);
260
+ if (signaturesEqual(headSymbols, stagedSymbols)) {
261
+ continue;
262
+ }
263
+ entries.push({
264
+ sourcePath: sourceFile.path,
265
+ entityIds: getEntityIdsForSourceFile(stagedManifestRecords, sourceFile.path),
266
+ });
267
+ }
268
+ return { path: paths.symbolsPath, entries };
269
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"symbol-extract.d.ts","sourceRoot":"","sources":["../../src/traceability/symbol-extract.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7D,KAAK,wBAAwB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAU7D,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,GAAG,OAAO,GAAG,UAAU,GAAG,MAAM,GAAG,SAAS,CAAC;IAC7D,QAAQ,EAAE;QACR,IAAI,EAAE,MAAM,CAAC;QACb,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,aAAa,CAAC,EAAE,wBAAwB,EAAE,CAAC;CAC5C;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,aAAa,CAAC,EAAE,wBAAwB,EAAE,CAAC;CAC5C;AAED,MAAM,MAAM,cAAc,GAAG,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;AAE9D,wBAAgB,+BAA+B,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAG5E;AA2LD,wBAAgB,4BAA4B,CAE1C,UAAU,EAAE,UAAU,EACtB,cAAc,CAAC,EAAE,cAAc,GAC9B,eAAe,EAAE,CA+JnB"}
1
+ {"version":3,"file":"symbol-extract.d.ts","sourceRoot":"","sources":["../../src/traceability/symbol-extract.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7D,KAAK,wBAAwB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAU7D,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,GAAG,OAAO,GAAG,UAAU,GAAG,MAAM,GAAG,SAAS,CAAC;IAC7D,QAAQ,EAAE;QACR,IAAI,EAAE,MAAM,CAAC;QACb,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,aAAa,CAAC,EAAE,wBAAwB,EAAE,CAAC;CAC5C;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,aAAa,CAAC,EAAE,wBAAwB,EAAE,CAAC;CAC5C;AAED,MAAM,MAAM,cAAc,GAAG,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;AAE9D,wBAAgB,+BAA+B,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAG5E;AA+ND,wBAAgB,4BAA4B,CAE1C,UAAU,EAAE,UAAU,EACtB,cAAc,CAAC,EAAE,cAAc,GAC9B,eAAe,EAAE,CA+JnB"}
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { Project, ScriptKind } from "ts-morph";
3
- import { extractFromManifest } from "../extractors/manifest.js";
3
+ import { readManifestWithCoordinateOverlay } from "../extractors/manifest.js";
4
4
  const TRACEABILITY_RELATIONSHIP_TYPES = new Set([
5
5
  "implements",
6
6
  "covered_by",
@@ -55,15 +55,12 @@ function resolveSymbolTraceability(filePath, name, manifestLookup) {
55
55
  }
56
56
  for (const manifestPath of candidateManifestPaths) {
57
57
  try {
58
- const ents = extractFromManifest(manifestPath);
59
- for (const e of ents) {
60
- if (e.entity.title === name) {
58
+ const records = readManifestWithCoordinateOverlay(manifestPath);
59
+ for (const record of records) {
60
+ if (record.title === name && typeof record.id === "string") {
61
61
  return {
62
- id: e.entity.id,
63
- relationships: filterTraceabilityRelationships(e.relationships.map((relationship) => ({
64
- type: relationship.type,
65
- to: relationship.to,
66
- }))),
62
+ id: record.id,
63
+ relationships: filterTraceabilityRelationships(extractRelationshipsFromManifestRecord(record)),
67
64
  };
68
65
  }
69
66
  }
@@ -74,6 +71,33 @@ function resolveSymbolTraceability(filePath, name, manifestLookup) {
74
71
  }
75
72
  return { id: createHashFallbackId(filePath, name) };
76
73
  }
74
+ function extractRelationshipsFromManifestRecord(record) {
75
+ const relationships = [];
76
+ if (Array.isArray(record.links)) {
77
+ for (const link of record.links) {
78
+ if (typeof link === "string") {
79
+ relationships.push({ type: "implements", to: link });
80
+ continue;
81
+ }
82
+ if (link &&
83
+ typeof link === "object" &&
84
+ typeof link.type === "string" &&
85
+ typeof link.target === "string") {
86
+ relationships.push({ type: link.type, to: link.target });
87
+ }
88
+ }
89
+ }
90
+ if (Array.isArray(record.relationships)) {
91
+ for (const relationship of record.relationships) {
92
+ if (relationship &&
93
+ typeof relationship.type === "string" &&
94
+ typeof relationship.target === "string") {
95
+ relationships.push({ type: relationship.type, to: relationship.target });
96
+ }
97
+ }
98
+ }
99
+ return relationships;
100
+ }
77
101
  function buildSymbolResult(stagedFile, name, kind, span, inlineReqLinks, manifestLookup) {
78
102
  const { id, relationships } = resolveSymbolTraceability(stagedFile.path, name, manifestLookup);
79
103
  const manifestReqLinks = getRequirementLinks(relationships);
@@ -0,0 +1,8 @@
1
+ export declare const DEFAULT_SYMBOLS_PATH = "documentation/symbols.yaml";
2
+ export declare const DEFAULT_COORDINATES_PATH = "documentation/symbol-coordinates.yaml";
3
+ export declare function resolveSymbolsManifestPaths(workspaceRoot: string, configPath?: string): {
4
+ symbolsPath: string;
5
+ coordinatesPath: string;
6
+ };
7
+ export declare function resolveSymbolsManifestPath(workspaceRoot: string, configPath?: string): string;
8
+ //# sourceMappingURL=manifest-paths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest-paths.d.ts","sourceRoot":"","sources":["../../src/utils/manifest-paths.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,oBAAoB,+BAA+B,CAAC;AACjE,eAAO,MAAM,wBAAwB,0CAA0C,CAAC;AAuEhF,wBAAgB,2BAA2B,CACzC,aAAa,EAAE,MAAM,EACrB,UAAU,CAAC,EAAE,MAAM,GAClB;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,MAAM,CAAA;CAAE,CASlD;AAGD,wBAAgB,0BAA0B,CACxC,aAAa,EAAE,MAAM,EACrB,UAAU,CAAC,EAAE,MAAM,GAClB,MAAM,CAER"}