kibi-cli 2.25.0 → 2.26.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.
- package/dist/cli-errors.d.ts +14 -0
- package/dist/cli-errors.d.ts.map +1 -1
- package/dist/cli-errors.js +31 -0
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +28 -0
- package/dist/commands/migrate.d.ts +2 -0
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +115 -4
- package/dist/commands/sync/cache.d.ts +30 -0
- package/dist/commands/sync/cache.d.ts.map +1 -1
- package/dist/commands/sync/cache.js +61 -0
- package/dist/commands/sync/extraction.d.ts +6 -0
- package/dist/commands/sync/extraction.d.ts.map +1 -1
- package/dist/commands/sync/extraction.js +4 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +27 -1
- package/dist/operations/bootstrap/claim-restatement.d.ts +37 -0
- package/dist/operations/bootstrap/claim-restatement.d.ts.map +1 -0
- package/dist/operations/bootstrap/claim-restatement.js +83 -0
- package/dist/operations/bootstrap/generate.d.ts.map +1 -1
- package/dist/operations/bootstrap/generate.js +23 -12
- package/dist/operations/bootstrap/intent-claims.d.ts +5 -2
- package/dist/operations/bootstrap/intent-claims.d.ts.map +1 -1
- package/dist/operations/bootstrap/intent-claims.js +44 -4
- package/dist/operations/migration/requirement-status.d.ts +41 -2
- package/dist/operations/migration/requirement-status.d.ts.map +1 -1
- package/dist/operations/migration/requirement-status.js +75 -7
- package/dist/operations/migration/schema6.d.ts +9 -5
- package/dist/operations/migration/schema6.d.ts.map +1 -1
- package/dist/operations/migration/schema6.js +82 -13
- package/dist/operations/migration/symbol-coordinates-refresh.d.ts +36 -0
- package/dist/operations/migration/symbol-coordinates-refresh.d.ts.map +1 -0
- package/dist/operations/migration/symbol-coordinates-refresh.js +99 -0
- package/dist/operations/mutation/source-authoring.d.ts +9 -0
- package/dist/operations/mutation/source-authoring.d.ts.map +1 -1
- package/dist/operations/mutation/source-authoring.js +20 -0
- package/dist/operations/mutation/stored-entity.d.ts +5 -0
- package/dist/operations/mutation/stored-entity.d.ts.map +1 -1
- package/dist/operations/mutation/uncompiled-source.d.ts +43 -0
- package/dist/operations/mutation/uncompiled-source.d.ts.map +1 -0
- package/dist/operations/mutation/uncompiled-source.js +115 -0
- package/dist/operations/mutation/upsert.d.ts +6 -0
- package/dist/operations/mutation/upsert.d.ts.map +1 -1
- package/dist/operations/mutation/upsert.js +32 -5
- package/dist/public/impact/analyzer.d.ts.map +1 -1
- package/dist/public/impact/analyzer.js +13 -3
- package/dist/public/impact/diagnostics.js +1 -1
- package/dist/public/impact/unowned-files.d.ts +31 -0
- package/dist/public/impact/unowned-files.d.ts.map +1 -0
- package/dist/public/impact/unowned-files.js +171 -0
- package/dist/public/operations/catalog.d.ts +1 -1
- package/dist/public/operations/discovery-entities.d.ts +11 -0
- package/dist/public/operations/discovery-entities.d.ts.map +1 -1
- package/dist/public/operations/discovery-entities.js +28 -0
- package/dist/public/operations/discovery-executors.d.ts.map +1 -1
- package/dist/public/operations/discovery-executors.js +13 -1
- package/dist/public/operations/result-envelope.d.ts +2 -0
- package/dist/public/operations/result-envelope.d.ts.map +1 -1
- package/dist/public/operations/result-envelope.js +1 -0
- package/dist/public/operations/source-relationship-parity.d.ts.map +1 -1
- package/dist/public/operations/source-relationship-parity.js +22 -7
- package/dist/public/operations/specs/check.d.ts +1 -1
- package/dist/public/operations/specs/check.js +1 -1
- package/dist/public/skills/kibi-bootstrap/SKILL.md +2 -2
- package/dist/public/skills/kibi-usage/SKILL.md +11 -5
- package/dist/traceability/git-change-snapshot.d.ts.map +1 -1
- package/dist/traceability/git-change-snapshot.js +44 -3
- package/dist/traceability/staged-diagnostics.d.ts +1 -1
- package/dist/traceability/staged-diagnostics.d.ts.map +1 -1
- package/dist/traceability/staged-migration.d.ts +13 -0
- package/dist/traceability/staged-migration.d.ts.map +1 -0
- package/dist/traceability/staged-migration.js +59 -0
- package/dist/traceability/staged-symbols-manifest.d.ts +11 -0
- package/dist/traceability/staged-symbols-manifest.d.ts.map +1 -1
- package/dist/traceability/staged-symbols-manifest.js +50 -16
- package/package.json +2 -2
- package/src/public/impact/analyzer.ts +21 -7
- package/src/public/impact/diagnostics.ts +1 -1
- package/src/public/impact/unowned-files.ts +229 -0
- package/src/public/operations/discovery-entities.ts +36 -0
- package/src/public/operations/discovery-executors.ts +17 -1
- package/src/public/operations/result-envelope.ts +3 -0
- package/src/public/operations/source-relationship-parity.ts +22 -7
- package/src/public/operations/specs/check.ts +1 -1
- package/src/public/skills/kibi-bootstrap/SKILL.md +2 -2
- package/src/public/skills/kibi-usage/SKILL.md +11 -5
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
id: kibi-usage
|
|
3
3
|
name: Kibi Usage
|
|
4
4
|
description: Use Kibi's source-first, exact-Git, migration-aware, proof-aware operations safely across MCP or the trusted local CLI, including partial completion repair.
|
|
5
|
-
version: 2.
|
|
5
|
+
version: 2.19.0
|
|
6
6
|
kibiCompatibility: ">=1.0.0"
|
|
7
7
|
tags:
|
|
8
8
|
- kibi
|
|
@@ -112,9 +112,11 @@ it is a pointer to the knowledge, never proof. Each line answers one question:
|
|
|
112
112
|
A file already shown this session gets only the one-line `Kibi: this edit to
|
|
113
113
|
<path> is inside <symbol>` update; the facts and tests from its first snippet
|
|
114
114
|
still apply. `Kibi: no symbol in <path> is linked to a requirement yet` means
|
|
115
|
-
the file is unowned
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
the file is unowned, not that no rule covers it. When the note names
|
|
116
|
+
requirements whose titles share the path's words, `kb_query` each one and
|
|
117
|
+
treat a match as the governing rule; otherwise find it with `kb_search` and
|
|
118
|
+
`sourceLocations`. Link the symbol that implements it (for markup, the
|
|
119
|
+
component that renders it) with `implements` through `kb_upsert`. Without a readable manifest (`Kibi: the hook could not read
|
|
118
120
|
.kb/symbols.yaml`) snippets are off for this session; `kb_query({sourceFile})`
|
|
119
121
|
still answers and `kibi sync` regenerates the manifest. The search tip and the
|
|
120
122
|
`.kb/` note appear once per session and need no action.
|
|
@@ -136,7 +138,11 @@ still answers and `kibi sync` regenerates the manifest. The search tip and the
|
|
|
136
138
|
2. Resolve genuine ambiguity with the human; use `kb_model` for deterministic
|
|
137
139
|
interpretation and typed facts.
|
|
138
140
|
3. Create endpoints before relationships and upsert small batches sequentially.
|
|
139
|
-
Keep the canonical `REQ-* -> SCEN-* -> TEST-*` chain.
|
|
141
|
+
Keep the canonical `REQ-* -> SCEN-* -> TEST-*` chain. Recording the
|
|
142
|
+
behavior a change adds (a requirement with a scenario, and an `implements`
|
|
143
|
+
link from the symbol) is part of finishing that change, not a separate
|
|
144
|
+
request: do it without asking. Ask only when the change conflicts with a
|
|
145
|
+
recorded rule.
|
|
140
146
|
4. Author tracked Markdown/YAML/manifests and relationship shards through Kibi;
|
|
141
147
|
Kibi never Git-stages or commits those files. Do not read or edit `.kb`
|
|
142
148
|
directly.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"git-change-snapshot.d.ts","sourceRoot":"","sources":["../../src/traceability/git-change-snapshot.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAIV,UAAU,EAEX,MAAM,iBAAiB,CAAC;AAKzB,MAAM,MAAM,iBAAiB,GAAG,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,MAAM,CAAC;AAGpE,MAAM,WAAW,iBAAiB;IAChC,iFAAiF;IACjF,QAAQ,EAAE,MAAM,CAAC;IACjB,0EAA0E;IAC1E,QAAQ,EAAE,MAAM,CAAC;IACjB,8DAA8D;IAC9D,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,gEAAgE;IAChE,OAAO,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAAC;IACzC,uEAAuE;IACvE,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChE,iEAAiE;IACjE,eAAe,IAAI,IAAI,CAAC;IACxB,SAAS,EAAE,UAAU,EAAE,CAAC;IACxB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC;;;;;OAKG;IACH,cAAc,CAAC,EAAE,aAAa,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC9D;AAueD;;;;;;;GAOG;AAEH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,SAAS,MAAM,EAAE,GACzB;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,IAAI,CA6ClD;
|
|
1
|
+
{"version":3,"file":"git-change-snapshot.d.ts","sourceRoot":"","sources":["../../src/traceability/git-change-snapshot.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAIV,UAAU,EAEX,MAAM,iBAAiB,CAAC;AAKzB,MAAM,MAAM,iBAAiB,GAAG,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,MAAM,CAAC;AAGpE,MAAM,WAAW,iBAAiB;IAChC,iFAAiF;IACjF,QAAQ,EAAE,MAAM,CAAC;IACjB,0EAA0E;IAC1E,QAAQ,EAAE,MAAM,CAAC;IACjB,8DAA8D;IAC9D,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,gEAAgE;IAChE,OAAO,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAAC;IACzC,uEAAuE;IACvE,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChE,iEAAiE;IACjE,eAAe,IAAI,IAAI,CAAC;IACxB,SAAS,EAAE,UAAU,EAAE,CAAC;IACxB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC;;;;;OAKG;IACH,cAAc,CAAC,EAAE,aAAa,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC9D;AAueD;;;;;;;GAOG;AAEH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,SAAS,MAAM,EAAE,GACzB;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,IAAI,CA6ClD;AA0HD;;;;;;;;;GASG;AAEH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GACf;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,GAAG,IAAI,CA2B3E;AAED,gFAAgF;AAEhF,wBAAgB,qBAAqB,CACnC,aAAa,EAAE,MAAM,GACpB,iBAAiB,CAuCnB;AAED;;;;;GAKG;AAEH,wBAAgB,mBAAmB,CACjC,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,iBAAiB,CAyBnB"}
|
|
@@ -509,6 +509,45 @@ function isKnowledgeRelocation(from, to) {
|
|
|
509
509
|
return (ENTITY_LANES.includes(lane) &&
|
|
510
510
|
`/${from}`.includes(`/${lane}/`));
|
|
511
511
|
}
|
|
512
|
+
/**
|
|
513
|
+
* Relocations into `.kb/` that Git's similarity detection missed: a deleted
|
|
514
|
+
* knowledge file and an added canonical file with the same file name, paired
|
|
515
|
+
* only when the pairing is unambiguous. A small file that a migration both
|
|
516
|
+
* moves and rewrites (a symbols manifest that gains a header and
|
|
517
|
+
* legacy-link marks, a requirement that gains origin and tags) can fall
|
|
518
|
+
* under Git's similarity threshold although it is the same file.
|
|
519
|
+
*/
|
|
520
|
+
// implements REQ-cli-migration-first-commit
|
|
521
|
+
function unpairedKnowledgeRelocations(root, baseTree, headTree, detected) {
|
|
522
|
+
const rows = parseNameStatus(runGit(root, [
|
|
523
|
+
"diff",
|
|
524
|
+
"--name-status",
|
|
525
|
+
"-z",
|
|
526
|
+
"--no-renames",
|
|
527
|
+
"--diff-filter=AD",
|
|
528
|
+
baseTree,
|
|
529
|
+
headTree,
|
|
530
|
+
]));
|
|
531
|
+
const paired = new Set(detected.flatMap((rename) => [rename.from, rename.to]));
|
|
532
|
+
const deleted = rows
|
|
533
|
+
.filter((row) => row.status === "D" && !paired.has(row.path))
|
|
534
|
+
.map((row) => row.path);
|
|
535
|
+
const added = rows
|
|
536
|
+
.filter((row) => row.status === "A" && !paired.has(row.path))
|
|
537
|
+
.map((row) => row.path);
|
|
538
|
+
const candidates = (to) => deleted.filter((from) => path.posix.basename(from) === path.posix.basename(to) &&
|
|
539
|
+
isKnowledgeRelocation(from, to));
|
|
540
|
+
const relocations = [];
|
|
541
|
+
for (const to of added) {
|
|
542
|
+
const [from, ...others] = candidates(to);
|
|
543
|
+
if (from === undefined || others.length > 0)
|
|
544
|
+
continue;
|
|
545
|
+
if (added.filter((other) => candidates(other).includes(from)).length !== 1)
|
|
546
|
+
continue;
|
|
547
|
+
relocations.push({ from, to });
|
|
548
|
+
}
|
|
549
|
+
return relocations;
|
|
550
|
+
}
|
|
512
551
|
/**
|
|
513
552
|
* For a staged snapshot, the tree a commit that relocates knowledge files
|
|
514
553
|
* into `.kb/` is judged against: `baseTree` with each relocated file's
|
|
@@ -524,9 +563,11 @@ export function renameResolutionBase(root, baseTree, headTree) {
|
|
|
524
563
|
const zero = "0".repeat(baseTree.length);
|
|
525
564
|
const renames = [];
|
|
526
565
|
const entries = [];
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
566
|
+
const detected = renamedPaths(root, baseTree, headTree).filter((rename) => isKnowledgeRelocation(rename.from, rename.to));
|
|
567
|
+
for (const rename of [
|
|
568
|
+
...detected,
|
|
569
|
+
...unpairedKnowledgeRelocations(root, baseTree, headTree, detected),
|
|
570
|
+
]) {
|
|
530
571
|
const source = readTreeEntry(root, baseTree, rename.from);
|
|
531
572
|
if (source?.type !== "blob")
|
|
532
573
|
continue;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { QualityDiagnostic } from "../public/impact/types.js";
|
|
2
2
|
import { type KibiImpactEvidence } from "./evidence-model.js";
|
|
3
|
-
export type KibiImpactDiagnosticId = "kibi_impact_evidence_missing" | "symbols_manifest_stale" | "symbol_granularity_violation" | "symbol_semantic_review_needed" | "multi_requirement_symbol_review" | "duplicate_symbol_coordinate_review" | "component_mixed_purpose_review" | "broad_requirement_review" | "requirement_status_review" | "strict_fact_modeling_review" | "logical_coverage_review" | "kibi_impact_override_missing_rationale" | "entity_id_style_review";
|
|
3
|
+
export type KibiImpactDiagnosticId = "kibi_impact_evidence_missing" | "symbols_manifest_stale" | "symbol_granularity_violation" | "symbol_semantic_review_needed" | "source_file_unowned" | "multi_requirement_symbol_review" | "duplicate_symbol_coordinate_review" | "component_mixed_purpose_review" | "broad_requirement_review" | "requirement_status_review" | "strict_fact_modeling_review" | "logical_coverage_review" | "kibi_impact_override_missing_rationale" | "entity_id_style_review";
|
|
4
4
|
export interface KibiImpactDiagnostic extends QualityDiagnostic {
|
|
5
5
|
/** Stable staged-enforcement diagnostic identifier. */
|
|
6
6
|
id: KibiImpactDiagnosticId;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"staged-diagnostics.d.ts","sourceRoot":"","sources":["../../src/traceability/staged-diagnostics.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,EAGL,KAAK,kBAAkB,EAKxB,MAAM,qBAAqB,CAAC;AAQ7B,MAAM,MAAM,sBAAsB,GAC9B,8BAA8B,GAC9B,wBAAwB,GACxB,8BAA8B,GAC9B,+BAA+B,GAC/B,iCAAiC,GACjC,oCAAoC,GACpC,gCAAgC,GAChC,0BAA0B,GAC1B,2BAA2B,GAC3B,6BAA6B,GAC7B,yBAAyB,GACzB,wCAAwC,GACxC,wBAAwB,CAAC;AAE7B,MAAM,WAAW,oBAAqB,SAAQ,iBAAiB;IAC7D,uDAAuD;IACvD,EAAE,EAAE,sBAAsB,CAAC;IAC3B,QAAQ,EAAE,iBAAiB,CAAC,UAAU,CAAC,CAAC;IACxC,iEAAiE;IACjE,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,gDAAgD;IAChD,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B;AAsID,wBAAgB,wCAAwC,CACtD,QAAQ,EAAE,kBAAkB,GAC3B,oBAAoB,CAoBtB;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,kBAAkB,EAC5B,mBAAmB,GAAE,MAAmC,GACvD,oBAAoB,EAAE,CAiDxB"}
|
|
1
|
+
{"version":3,"file":"staged-diagnostics.d.ts","sourceRoot":"","sources":["../../src/traceability/staged-diagnostics.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,EAGL,KAAK,kBAAkB,EAKxB,MAAM,qBAAqB,CAAC;AAQ7B,MAAM,MAAM,sBAAsB,GAC9B,8BAA8B,GAC9B,wBAAwB,GACxB,8BAA8B,GAC9B,+BAA+B,GAC/B,qBAAqB,GACrB,iCAAiC,GACjC,oCAAoC,GACpC,gCAAgC,GAChC,0BAA0B,GAC1B,2BAA2B,GAC3B,6BAA6B,GAC7B,yBAAyB,GACzB,wCAAwC,GACxC,wBAAwB,CAAC;AAE7B,MAAM,WAAW,oBAAqB,SAAQ,iBAAiB;IAC7D,uDAAuD;IACvD,EAAE,EAAE,sBAAsB,CAAC;IAC3B,QAAQ,EAAE,iBAAiB,CAAC,UAAU,CAAC,CAAC;IACxC,iEAAiE;IACjE,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,gDAAgD;IAChD,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B;AAsID,wBAAgB,wCAAwC,CACtD,QAAQ,EAAE,kBAAkB,GAC3B,oBAAoB,CAoBtB;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,kBAAkB,EAC5B,mBAAmB,GAAE,MAAmC,GACvD,oBAAoB,EAAE,CAiDxB"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True when the staged commit carries a `kibi migrate` run: the staged
|
|
3
|
+
* `.kb/manifest.json` records a schema generation newer than the base, or the
|
|
4
|
+
* commit retires the legacy `.kb/config.json`. The audit record migrate writes
|
|
5
|
+
* under `.kb/migrations/` is ignored by Git, so the schema bump is the
|
|
6
|
+
* committed evidence; it cannot be repeated once a KB is at the latest schema,
|
|
7
|
+
* so it cannot be used to waive later edits.
|
|
8
|
+
*
|
|
9
|
+
* `readBase` and `readStaged` return a file's content in the base and the
|
|
10
|
+
* staged tree, or null when the file does not exist there.
|
|
11
|
+
*/
|
|
12
|
+
export declare function stagedCommitCarriesMigration(readBase: (filePath: string) => string | null, readStaged: (filePath: string) => string | null): boolean;
|
|
13
|
+
//# sourceMappingURL=staged-migration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"staged-migration.d.ts","sourceRoot":"","sources":["../../src/traceability/staged-migration.ts"],"names":[],"mappings":"AAoCA;;;;;;;;;;GAUG;AAEH,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,EAC7C,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,GAC9C,OAAO,CAWT"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Kibi — repo-local, per-branch, queryable long-term memory for software projects
|
|
3
|
+
Copyright (C) 2026 Piotr Franczyk
|
|
4
|
+
|
|
5
|
+
This program is free software: you can redistribute it and/or modify
|
|
6
|
+
it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
(at your option) any later version.
|
|
9
|
+
|
|
10
|
+
This program is distributed in the hope that it will be useful,
|
|
11
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
GNU Affero General Public License for more details.
|
|
14
|
+
|
|
15
|
+
You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
*/
|
|
18
|
+
import { normalizeSchemaVersion } from "../utils/schema-version.js";
|
|
19
|
+
const KB_MANIFEST_PATH = ".kb/manifest.json";
|
|
20
|
+
const LEGACY_CONFIG_PATH = ".kb/config.json";
|
|
21
|
+
function schemaVersionOf(text) {
|
|
22
|
+
if (text === null)
|
|
23
|
+
return null;
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(text);
|
|
26
|
+
const raw = parsed?.schemaVersion;
|
|
27
|
+
return typeof raw === "number" || typeof raw === "string"
|
|
28
|
+
? normalizeSchemaVersion(raw)
|
|
29
|
+
: null;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* True when the staged commit carries a `kibi migrate` run: the staged
|
|
37
|
+
* `.kb/manifest.json` records a schema generation newer than the base, or the
|
|
38
|
+
* commit retires the legacy `.kb/config.json`. The audit record migrate writes
|
|
39
|
+
* under `.kb/migrations/` is ignored by Git, so the schema bump is the
|
|
40
|
+
* committed evidence; it cannot be repeated once a KB is at the latest schema,
|
|
41
|
+
* so it cannot be used to waive later edits.
|
|
42
|
+
*
|
|
43
|
+
* `readBase` and `readStaged` return a file's content in the base and the
|
|
44
|
+
* staged tree, or null when the file does not exist there.
|
|
45
|
+
*/
|
|
46
|
+
// implements REQ-cli-migration-first-commit
|
|
47
|
+
export function stagedCommitCarriesMigration(readBase, readStaged) {
|
|
48
|
+
const staged = schemaVersionOf(readStaged(KB_MANIFEST_PATH));
|
|
49
|
+
if (staged === null)
|
|
50
|
+
return false;
|
|
51
|
+
const baseConfig = readBase(LEGACY_CONFIG_PATH);
|
|
52
|
+
if (baseConfig !== null && readStaged(LEGACY_CONFIG_PATH) === null)
|
|
53
|
+
return true;
|
|
54
|
+
const baseVersions = [
|
|
55
|
+
schemaVersionOf(readBase(KB_MANIFEST_PATH)),
|
|
56
|
+
schemaVersionOf(baseConfig),
|
|
57
|
+
].filter((version) => version !== null);
|
|
58
|
+
return baseVersions.length === 0 || Math.min(...baseVersions) < staged;
|
|
59
|
+
}
|
|
@@ -20,6 +20,11 @@ export interface StagedAuthoredSymbolsManifestEvidence {
|
|
|
20
20
|
entityIds: string[];
|
|
21
21
|
}>;
|
|
22
22
|
changedEntityIds: string[];
|
|
23
|
+
/**
|
|
24
|
+
* Records whose only differences from the base are fields `kibi migrate`
|
|
25
|
+
* writes mechanically; set only for a commit that carries a migration.
|
|
26
|
+
*/
|
|
27
|
+
migrationOnlyEntityIds?: string[];
|
|
23
28
|
}
|
|
24
29
|
export declare function parsedCoordinateOrNull(parse: () => ParsedCoordinateArtifact): ParsedCoordinateArtifact | null;
|
|
25
30
|
export declare function manifestRecordSourcePath(record: {
|
|
@@ -37,5 +42,11 @@ export declare function collectStagedAuthoredSymbolsManifestEvidence(options: {
|
|
|
37
42
|
sourceFiles: StagedFile[];
|
|
38
43
|
stagedFiles: StagedFile[];
|
|
39
44
|
readBaseFile?: (filePath: string) => string | null;
|
|
45
|
+
/**
|
|
46
|
+
* The commit carries a `kibi migrate` run: records it changed only in
|
|
47
|
+
* migration-owned fields are reported in `migrationOnlyEntityIds` instead
|
|
48
|
+
* of `changedEntityIds`.
|
|
49
|
+
*/
|
|
50
|
+
ignoreMigrationOwnedEdits?: boolean;
|
|
40
51
|
}): StagedAuthoredSymbolsManifestEvidence;
|
|
41
52
|
//# sourceMappingURL=staged-symbols-manifest.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"staged-symbols-manifest.d.ts","sourceRoot":"","sources":["../../src/traceability/staged-symbols-manifest.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,oBAAoB,EAErB,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"staged-symbols-manifest.d.ts","sourceRoot":"","sources":["../../src/traceability/staged-symbols-manifest.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,oBAAoB,EAErB,MAAM,iBAAiB,CAAC;AAMzB,OAAO,EACL,KAAK,wBAAwB,EAG9B,MAAM,qCAAqC,CAAC;AAI7C,OAAO,KAAK,EAAE,mCAAmC,EAAE,MAAM,qBAAqB,CAAC;AAC/E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AA0ClD,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;IACb;;;;OAIG;IACH,WAAW,CAAC,EAAE,mCAAmC,EAAE,CAAC;CACrD;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;IAC5D,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B;;;OAGG;IACH,sBAAsB,CAAC,EAAE,MAAM,EAAE,CAAC;CACnC;AA6ED,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,MAAM,wBAAwB,GACpC,wBAAwB,GAAG,IAAI,CAOjC;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE;IAC/C,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,GAAG,MAAM,GAAG,IAAI,CAMhB;AA2VD,wBAAgB,2BAA2B,CAAC,OAAO,EAAE;IACnD,mBAAmB,EAAE,MAAM,CAAC;IAC5B,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACnD,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,oBAAoB,CAAC;CAC7E,GAAG,+BAA+B,CAoLlC;AAED,wBAAgB,4CAA4C,CAAC,OAAO,EAAE;IACpE,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACnD;;;;OAIG;IACH,yBAAyB,CAAC,EAAE,OAAO,CAAC;CACrC,GAAG,qCAAqC,CA4ExC"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import { CONTEXT_MISSING_TAG } from "../entity-body-context.js";
|
|
3
4
|
import { extractManifestSymbolRecordsString, } from "../extractors/manifest.js";
|
|
4
5
|
import { mergeCoordinatesWithManifest, parseCoordinateArtifact as parseStrict, } from "../extractors/symbol-coordinates.js";
|
|
5
6
|
import { analyzeSourceText } from "../extractors/symbols-coordinator.js";
|
|
@@ -199,21 +200,50 @@ function normalizeAuthoredManifestSymbolsForSourceFile(records, sourceFile) {
|
|
|
199
200
|
function getAuthoredSymbolComparisonKey(symbol) {
|
|
200
201
|
return symbol.id ?? `${symbol.sourceFile}:${symbol.title}`;
|
|
201
202
|
}
|
|
202
|
-
|
|
203
|
-
|
|
203
|
+
/**
|
|
204
|
+
* The staged record with the edits `kibi migrate` makes mechanically undone
|
|
205
|
+
* against its base record: a `granularity_reason: legacy-link` the base did
|
|
206
|
+
* not have, and a `review:context-missing` tag the base did not carry. The
|
|
207
|
+
* other migration-owned fields (`origin`, a removed dead `source`, a pinned
|
|
208
|
+
* `semantic_text`) are not part of the authored symbol signature.
|
|
209
|
+
*/
|
|
210
|
+
// implements REQ-cli-migration-first-commit
|
|
211
|
+
function withoutMigrationOwnedEdits(head, staged) {
|
|
212
|
+
return {
|
|
213
|
+
...staged,
|
|
214
|
+
granularityReason: staged.granularityReason === "legacy-link" &&
|
|
215
|
+
head.granularityReason === null
|
|
216
|
+
? null
|
|
217
|
+
: staged.granularityReason,
|
|
218
|
+
tags: staged.tags.filter((tag) => tag !== CONTEXT_MISSING_TAG || head.tags.includes(tag)),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function getChangedAuthoredEntityIds(headSymbols, stagedSymbols, ignoreMigrationOwnedEdits = false) {
|
|
222
|
+
const headByKey = new Map(headSymbols.map((symbol) => [
|
|
204
223
|
getAuthoredSymbolComparisonKey(symbol),
|
|
205
|
-
|
|
224
|
+
symbol,
|
|
206
225
|
]));
|
|
207
|
-
const
|
|
226
|
+
const changed = [];
|
|
227
|
+
const migrationOnly = [];
|
|
228
|
+
for (const symbol of stagedSymbols) {
|
|
208
229
|
if (symbol.id === null)
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
230
|
+
continue;
|
|
231
|
+
const head = headByKey.get(getAuthoredSymbolComparisonKey(symbol));
|
|
232
|
+
if (head !== undefined && JSON.stringify(head) === JSON.stringify(symbol))
|
|
233
|
+
continue;
|
|
234
|
+
if (ignoreMigrationOwnedEdits &&
|
|
235
|
+
head !== undefined &&
|
|
236
|
+
JSON.stringify(head) ===
|
|
237
|
+
JSON.stringify(withoutMigrationOwnedEdits(head, symbol))) {
|
|
238
|
+
migrationOnly.push(symbol.id);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
changed.push(symbol.id);
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
changed: uniqueSorted(changed),
|
|
245
|
+
migrationOnly: uniqueSorted(migrationOnly),
|
|
246
|
+
};
|
|
217
247
|
}
|
|
218
248
|
function normalizeExpectedSymbolsForStagedFile(stagedFile, analyze, authoredTitles = new Set()) {
|
|
219
249
|
const analysis = (analyze ?? analyzeSourceText)(stagedFile.path, stagedFile.content ?? "");
|
|
@@ -430,13 +460,14 @@ export function collectStagedAuthoredSymbolsManifestEvidence(options) {
|
|
|
430
460
|
: null;
|
|
431
461
|
return sourceFile === null ? [] : [sourceFile];
|
|
432
462
|
}));
|
|
463
|
+
const migrationOnlyEntityIds = new Set();
|
|
433
464
|
const changedEntityIdsBySourcePath = new Map(sourcePaths.map((sourcePath) => {
|
|
434
465
|
const headSymbols = normalizeAuthoredManifestSymbolsForSourceFile(headManifestRecords, sourcePath);
|
|
435
466
|
const stagedSymbols = normalizeAuthoredManifestSymbolsForSourceFile(stagedManifestRecords, sourcePath);
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
];
|
|
467
|
+
const { changed, migrationOnly } = getChangedAuthoredEntityIds(headSymbols, stagedSymbols, options.ignoreMigrationOwnedEdits === true);
|
|
468
|
+
for (const id of migrationOnly)
|
|
469
|
+
migrationOnlyEntityIds.add(id);
|
|
470
|
+
return [sourcePath, changed];
|
|
440
471
|
}));
|
|
441
472
|
const entries = [];
|
|
442
473
|
for (const sourceFile of sourceFiles) {
|
|
@@ -449,5 +480,8 @@ export function collectStagedAuthoredSymbolsManifestEvidence(options) {
|
|
|
449
480
|
path: paths.symbolsPath,
|
|
450
481
|
entries,
|
|
451
482
|
changedEntityIds: uniqueSorted(Array.from(changedEntityIdsBySourcePath.values()).flat()),
|
|
483
|
+
...(migrationOnlyEntityIds.size > 0
|
|
484
|
+
? { migrationOnlyEntityIds: uniqueSorted(migrationOnlyEntityIds) }
|
|
485
|
+
: {}),
|
|
452
486
|
};
|
|
453
487
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.26.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Kibi CLI for knowledge base management",
|
|
6
6
|
"engines": {
|
|
@@ -235,7 +235,7 @@
|
|
|
235
235
|
"fast-glob": "^3.2.12",
|
|
236
236
|
"gray-matter": "^4.0.3",
|
|
237
237
|
"js-yaml": "^4.1.0",
|
|
238
|
-
"kibi-core": "^0.22.
|
|
238
|
+
"kibi-core": "^0.22.2",
|
|
239
239
|
"kibi-plugin-builtin": "^0.5.2",
|
|
240
240
|
"kibi-plugin-sdk": "^0.5.0",
|
|
241
241
|
"kibi-swipl": "^1.0.1",
|
|
@@ -32,6 +32,7 @@ import type {
|
|
|
32
32
|
ChangedFileImpactOptions,
|
|
33
33
|
ChangedFileImpactResult,
|
|
34
34
|
} from "./types.js";
|
|
35
|
+
import { createUnownedFileDiagnostics } from "./unowned-files.js";
|
|
35
36
|
|
|
36
37
|
export async function analyzeChangedFileImpact(
|
|
37
38
|
options: ChangedFileImpactOptions,
|
|
@@ -139,10 +140,27 @@ export async function analyzeChangedFileImpact(
|
|
|
139
140
|
result.sourceFile !== undefined &&
|
|
140
141
|
activeSourceFiles.has(result.sourceFile),
|
|
141
142
|
);
|
|
143
|
+
const linkedSourceFiles = new Set(
|
|
144
|
+
manifestResults
|
|
145
|
+
.filter((result) =>
|
|
146
|
+
result.relationships.some((relationship) =>
|
|
147
|
+
isTraceabilityRelationshipType(relationship.type),
|
|
148
|
+
),
|
|
149
|
+
)
|
|
150
|
+
.map((result) => result.sourceFile)
|
|
151
|
+
.filter((file): file is string => file !== undefined),
|
|
152
|
+
);
|
|
142
153
|
const impactDiagnostics =
|
|
143
154
|
options.includeImpactDiagnostics === false
|
|
144
155
|
? []
|
|
145
156
|
: [
|
|
157
|
+
// Named files first: a file without links is the answer a scoped
|
|
158
|
+
// check most easily misreads as "no rule applies".
|
|
159
|
+
...createUnownedFileDiagnostics({
|
|
160
|
+
workspaceRoot,
|
|
161
|
+
requestedFiles: [...filter],
|
|
162
|
+
linkedSourceFiles,
|
|
163
|
+
}),
|
|
146
164
|
...createSymbolGranularityDiagnostics({
|
|
147
165
|
manifestResults: activeManifestResults,
|
|
148
166
|
symbolsByFile,
|
|
@@ -157,13 +175,9 @@ export async function analyzeChangedFileImpact(
|
|
|
157
175
|
...createSemanticReviewDiagnostics({
|
|
158
176
|
symbolsByFile,
|
|
159
177
|
linkedSourceFiles: new Set(
|
|
160
|
-
|
|
161
|
-
.
|
|
162
|
-
|
|
163
|
-
isTraceabilityRelationshipType(relationship.type),
|
|
164
|
-
),
|
|
165
|
-
)
|
|
166
|
-
.map((result) => result.sourceFile as string),
|
|
178
|
+
[...linkedSourceFiles].filter((file) =>
|
|
179
|
+
activeSourceFiles.has(file),
|
|
180
|
+
),
|
|
167
181
|
),
|
|
168
182
|
}),
|
|
169
183
|
];
|
|
@@ -207,7 +207,7 @@ export function createSemanticReviewDiagnostics(
|
|
|
207
207
|
? `Changed behavioral symbol ${symbol.name} is in a file no requirement owns, so no recorded intent covers the behavior it has after this change`
|
|
208
208
|
: `Changed behavioral symbol ${symbol.name} needs semantic review against linked Kibi coverage: ${linkedTargets}`,
|
|
209
209
|
suggestion: unlinked
|
|
210
|
-
? `If the change adds or alters behavior, record it
|
|
210
|
+
? `If the change adds or alters behavior, record it as part of this change; that is part of finishing it, not a separate request, so it needs no permission: kb_search({query:"<the behavior>", sourceLocations:[{path:"${file}"}]}) for a requirement that already describes it and link ${symbol.name} to it; if none does, kb_upsert a requirement with a scenario and link the symbol with implements. If behavior is unchanged (a refactor, renaming), say so in the final report instead.`
|
|
211
211
|
: `Use kb_search and kb_query for ${file}, then verify that ${linkedTargets} actually describe and test the changed behavior or UI copy. Update Kibi links or requirements/tests while the edit context is fresh.`,
|
|
212
212
|
});
|
|
213
213
|
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { closeSync, openSync, readSync, readdirSync } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { KibiImpactDiagnostic } from "../../traceability/staged-diagnostics.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A file `kb_check` was asked about that no symbol links to a requirement.
|
|
7
|
+
* No link is not no rule, so the check says so and names the current
|
|
8
|
+
* requirements whose titles share the file's path words: candidates to
|
|
9
|
+
* confirm, never owners. Kibi's hooks name the same candidates before an edit
|
|
10
|
+
* (kibi-agent-core `requirement-candidates`), with the same word rules.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const REQUIREMENTS_DIR = path.join(".kb", "requirements");
|
|
14
|
+
const HEAD_BYTES = 4096;
|
|
15
|
+
const MAX_CANDIDATES = 3;
|
|
16
|
+
const MAX_TITLE = 100;
|
|
17
|
+
|
|
18
|
+
/** Path words that name a layout, a language or a role, not a behavior. */
|
|
19
|
+
const LAYOUT_WORDS = new Set(
|
|
20
|
+
(
|
|
21
|
+
"src app apps lib libs source page pages component components view views " +
|
|
22
|
+
"screen screens package packages main index shared common core util utils " +
|
|
23
|
+
"helper helpers public static asset assets web client server frontend " +
|
|
24
|
+
"backend module modules feature features internal service services test " +
|
|
25
|
+
"tests spec specs dist build html htm tsx jsx mjs cjs vue svelte css scss " +
|
|
26
|
+
"less json yaml yml java swift php"
|
|
27
|
+
).split(" "),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
/** Words of a title or path that carry no topic. */
|
|
31
|
+
const FILLER_WORDS = new Set(
|
|
32
|
+
(
|
|
33
|
+
"the and for with must should shall may can are its their that this " +
|
|
34
|
+
"from into when each every all not req auto"
|
|
35
|
+
).split(" "),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const RETIRED_STATUS =
|
|
39
|
+
/supersed|deprecat|reject|obsolete|retired|archived|removed/i;
|
|
40
|
+
|
|
41
|
+
/** Lowercase word stems of an identifier, path segment or title. */
|
|
42
|
+
function words(text: string): string[] {
|
|
43
|
+
return text
|
|
44
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
45
|
+
.toLowerCase()
|
|
46
|
+
.split(/[^a-z0-9]+/)
|
|
47
|
+
.filter((word) => word.length >= 3 && !/\d/.test(word))
|
|
48
|
+
.map((word) =>
|
|
49
|
+
word.length > 4 && word.endsWith("s") && !word.endsWith("ss")
|
|
50
|
+
? word.slice(0, -1)
|
|
51
|
+
: word,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// implements REQ-kb-unowned-file-candidates
|
|
56
|
+
/**
|
|
57
|
+
* The topic words of a workspace path, without layout, language and role
|
|
58
|
+
* words (`src/app/pages/coach-dashboard/coach-dashboard.html` -> `coach`,
|
|
59
|
+
* `dashboard`).
|
|
60
|
+
*/
|
|
61
|
+
export function pathTopicWords(relativePath: string): string[] {
|
|
62
|
+
const segments = relativePath.split(/[\\/]/).filter(Boolean);
|
|
63
|
+
const last = segments.pop() ?? "";
|
|
64
|
+
const dot = last.indexOf(".");
|
|
65
|
+
const parts = dot >= 0 ? [last.slice(0, dot), last.slice(dot + 1)] : [last];
|
|
66
|
+
const found = [...segments, ...parts]
|
|
67
|
+
.flatMap(words)
|
|
68
|
+
.filter((word) => !LAYOUT_WORDS.has(word) && !FILLER_WORDS.has(word));
|
|
69
|
+
return [...new Set(found)];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// implements REQ-kb-unowned-file-candidates
|
|
73
|
+
export type RequirementCandidate = {
|
|
74
|
+
readonly id: string;
|
|
75
|
+
readonly title: string;
|
|
76
|
+
readonly status?: string;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// implements REQ-kb-unowned-file-candidates
|
|
80
|
+
/**
|
|
81
|
+
* Current requirements ranked by how many of the path's topic words their
|
|
82
|
+
* title shares, then by how often the title uses them, then by id; only
|
|
83
|
+
* those sharing at least one word.
|
|
84
|
+
*/
|
|
85
|
+
export function rankRequirementCandidates(
|
|
86
|
+
relativePath: string,
|
|
87
|
+
requirements: readonly RequirementCandidate[],
|
|
88
|
+
limit: number = MAX_CANDIDATES,
|
|
89
|
+
): RequirementCandidate[] {
|
|
90
|
+
const topic = new Set(pathTopicWords(relativePath));
|
|
91
|
+
if (topic.size === 0) return [];
|
|
92
|
+
const scored: {
|
|
93
|
+
requirement: RequirementCandidate;
|
|
94
|
+
shared: number;
|
|
95
|
+
uses: number;
|
|
96
|
+
}[] = [];
|
|
97
|
+
for (const requirement of requirements) {
|
|
98
|
+
if (
|
|
99
|
+
requirement.status !== undefined &&
|
|
100
|
+
RETIRED_STATUS.test(requirement.status)
|
|
101
|
+
)
|
|
102
|
+
continue;
|
|
103
|
+
const matched = words(requirement.title).filter((word) => topic.has(word));
|
|
104
|
+
const shared = new Set(matched).size;
|
|
105
|
+
if (shared > 0) scored.push({ requirement, shared, uses: matched.length });
|
|
106
|
+
}
|
|
107
|
+
scored.sort(
|
|
108
|
+
(a, b) =>
|
|
109
|
+
b.shared - a.shared ||
|
|
110
|
+
b.uses - a.uses ||
|
|
111
|
+
a.requirement.id.localeCompare(b.requirement.id),
|
|
112
|
+
);
|
|
113
|
+
return scored.slice(0, limit).map((entry) => entry.requirement);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function unquote(value: string): string {
|
|
117
|
+
const trimmed = value.trim();
|
|
118
|
+
return /^(["']).*\1$/.test(trimmed) ? trimmed.slice(1, -1) : trimmed;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function readRequirementHead(file: string): RequirementCandidate | undefined {
|
|
122
|
+
let head: string;
|
|
123
|
+
try {
|
|
124
|
+
const descriptor = openSync(file, "r");
|
|
125
|
+
try {
|
|
126
|
+
const buffer = Buffer.alloc(HEAD_BYTES);
|
|
127
|
+
const bytes = readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
128
|
+
head = buffer.subarray(0, bytes).toString("utf8");
|
|
129
|
+
} finally {
|
|
130
|
+
closeSync(descriptor);
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
const lines = head.split(/\r?\n/);
|
|
136
|
+
if (lines[0]?.trim() !== "---") return undefined;
|
|
137
|
+
const fields: Record<string, string> = {};
|
|
138
|
+
for (const line of lines.slice(1)) {
|
|
139
|
+
if (line.trim() === "---") break;
|
|
140
|
+
const match = /^(id|title|status):\s*(.+)$/.exec(line);
|
|
141
|
+
if (match?.[1] && match[2]) fields[match[1]] = unquote(match[2]);
|
|
142
|
+
}
|
|
143
|
+
if (!fields.id || !fields.title) return undefined;
|
|
144
|
+
return {
|
|
145
|
+
id: fields.id,
|
|
146
|
+
title: fields.title,
|
|
147
|
+
...(fields.status ? { status: fields.status } : {}),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function readRequirementHeads(workspaceRoot: string): RequirementCandidate[] {
|
|
152
|
+
const directory = path.join(workspaceRoot, REQUIREMENTS_DIR);
|
|
153
|
+
let names: string[];
|
|
154
|
+
try {
|
|
155
|
+
names = readdirSync(directory).filter((name) => name.endsWith(".md"));
|
|
156
|
+
} catch {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
return names
|
|
160
|
+
.map((name) => readRequirementHead(path.join(directory, name)))
|
|
161
|
+
.filter(
|
|
162
|
+
(requirement): requirement is RequirementCandidate =>
|
|
163
|
+
requirement !== undefined,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function truncate(text: string, limit: number): string {
|
|
168
|
+
return text.length > limit ? `${text.slice(0, limit - 1)}…` : text;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Files the unowned note does not apply to: the KB itself and prose. */
|
|
172
|
+
function isKnowledgeOrProse(file: string): boolean {
|
|
173
|
+
return file.startsWith(".kb/") || /\.mdx?$/i.test(file);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// implements REQ-kb-unowned-file-candidates
|
|
177
|
+
export type UnownedFileDiagnosticsOptions = {
|
|
178
|
+
readonly workspaceRoot: string;
|
|
179
|
+
/** Workspace-relative files the check was asked about. */
|
|
180
|
+
readonly requestedFiles: readonly string[];
|
|
181
|
+
/** Files where some manifest symbol carries a traceability link. */
|
|
182
|
+
readonly linkedSourceFiles: ReadonlySet<string>;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// implements REQ-kb-unowned-file-candidates
|
|
186
|
+
/**
|
|
187
|
+
* One `source_file_unowned` note per requested file without a linked symbol,
|
|
188
|
+
* naming the current requirements whose titles share its path words.
|
|
189
|
+
*/
|
|
190
|
+
export function createUnownedFileDiagnostics(
|
|
191
|
+
options: UnownedFileDiagnosticsOptions,
|
|
192
|
+
): KibiImpactDiagnostic[] {
|
|
193
|
+
const files = [...new Set(options.requestedFiles)].filter(
|
|
194
|
+
(file) => !options.linkedSourceFiles.has(file) && !isKnowledgeOrProse(file),
|
|
195
|
+
);
|
|
196
|
+
if (files.length === 0) return [];
|
|
197
|
+
const requirements = readRequirementHeads(options.workspaceRoot);
|
|
198
|
+
return files.map((file) => {
|
|
199
|
+
const candidates = rankRequirementCandidates(file, requirements);
|
|
200
|
+
const named = candidates
|
|
201
|
+
.map(
|
|
202
|
+
(candidate) =>
|
|
203
|
+
`${candidate.id} (${truncate(candidate.title, MAX_TITLE)})`,
|
|
204
|
+
)
|
|
205
|
+
.join("; ");
|
|
206
|
+
const lead = `No symbol in ${file} is linked to a requirement, so this check finds no owner for it; that does not mean no recorded rule covers the behavior it implements.`;
|
|
207
|
+
const record =
|
|
208
|
+
"If none does and the change adds behavior, record a requirement with a scenario and link it with implements as part of this change; if behavior is unchanged, say so in the final report.";
|
|
209
|
+
return {
|
|
210
|
+
id: "source_file_unowned",
|
|
211
|
+
severity: "info",
|
|
212
|
+
blocking: false,
|
|
213
|
+
category: "requirement",
|
|
214
|
+
files: [file],
|
|
215
|
+
docs: ["docs/symbol-traceability-taxonomy.md"],
|
|
216
|
+
message:
|
|
217
|
+
candidates.length > 0
|
|
218
|
+
? `${lead} Requirements whose titles share its path words: ${named}.`
|
|
219
|
+
: lead,
|
|
220
|
+
suggestion:
|
|
221
|
+
candidates.length > 0
|
|
222
|
+
? `kb_query each candidate to see whether it describes the changed behavior; when one does, link the symbol that implements it (for markup, the component that renders it) with implements and name the requirement in the final report. ${record}`
|
|
223
|
+
: `kb_search({query:"<the behavior>", sourceLocations:[{path:${JSON.stringify(file)}}]}) for a requirement that already describes it. ${record}`,
|
|
224
|
+
evidence: {
|
|
225
|
+
candidates: candidates.map(({ id, title }) => ({ id, title })),
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
});
|
|
229
|
+
}
|