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.
Files changed (86) hide show
  1. package/dist/cli-errors.d.ts +14 -0
  2. package/dist/cli-errors.d.ts.map +1 -1
  3. package/dist/cli-errors.js +31 -0
  4. package/dist/commands/check.d.ts.map +1 -1
  5. package/dist/commands/check.js +28 -0
  6. package/dist/commands/migrate.d.ts +2 -0
  7. package/dist/commands/migrate.d.ts.map +1 -1
  8. package/dist/commands/migrate.js +115 -4
  9. package/dist/commands/sync/cache.d.ts +30 -0
  10. package/dist/commands/sync/cache.d.ts.map +1 -1
  11. package/dist/commands/sync/cache.js +61 -0
  12. package/dist/commands/sync/extraction.d.ts +6 -0
  13. package/dist/commands/sync/extraction.d.ts.map +1 -1
  14. package/dist/commands/sync/extraction.js +4 -1
  15. package/dist/commands/sync.d.ts.map +1 -1
  16. package/dist/commands/sync.js +27 -1
  17. package/dist/operations/bootstrap/claim-restatement.d.ts +37 -0
  18. package/dist/operations/bootstrap/claim-restatement.d.ts.map +1 -0
  19. package/dist/operations/bootstrap/claim-restatement.js +83 -0
  20. package/dist/operations/bootstrap/generate.d.ts.map +1 -1
  21. package/dist/operations/bootstrap/generate.js +23 -12
  22. package/dist/operations/bootstrap/intent-claims.d.ts +5 -2
  23. package/dist/operations/bootstrap/intent-claims.d.ts.map +1 -1
  24. package/dist/operations/bootstrap/intent-claims.js +44 -4
  25. package/dist/operations/migration/requirement-status.d.ts +41 -2
  26. package/dist/operations/migration/requirement-status.d.ts.map +1 -1
  27. package/dist/operations/migration/requirement-status.js +75 -7
  28. package/dist/operations/migration/schema6.d.ts +9 -5
  29. package/dist/operations/migration/schema6.d.ts.map +1 -1
  30. package/dist/operations/migration/schema6.js +82 -13
  31. package/dist/operations/migration/symbol-coordinates-refresh.d.ts +36 -0
  32. package/dist/operations/migration/symbol-coordinates-refresh.d.ts.map +1 -0
  33. package/dist/operations/migration/symbol-coordinates-refresh.js +99 -0
  34. package/dist/operations/mutation/source-authoring.d.ts +9 -0
  35. package/dist/operations/mutation/source-authoring.d.ts.map +1 -1
  36. package/dist/operations/mutation/source-authoring.js +20 -0
  37. package/dist/operations/mutation/stored-entity.d.ts +5 -0
  38. package/dist/operations/mutation/stored-entity.d.ts.map +1 -1
  39. package/dist/operations/mutation/uncompiled-source.d.ts +43 -0
  40. package/dist/operations/mutation/uncompiled-source.d.ts.map +1 -0
  41. package/dist/operations/mutation/uncompiled-source.js +115 -0
  42. package/dist/operations/mutation/upsert.d.ts +6 -0
  43. package/dist/operations/mutation/upsert.d.ts.map +1 -1
  44. package/dist/operations/mutation/upsert.js +32 -5
  45. package/dist/public/impact/analyzer.d.ts.map +1 -1
  46. package/dist/public/impact/analyzer.js +13 -3
  47. package/dist/public/impact/diagnostics.js +1 -1
  48. package/dist/public/impact/unowned-files.d.ts +31 -0
  49. package/dist/public/impact/unowned-files.d.ts.map +1 -0
  50. package/dist/public/impact/unowned-files.js +171 -0
  51. package/dist/public/operations/catalog.d.ts +1 -1
  52. package/dist/public/operations/discovery-entities.d.ts +11 -0
  53. package/dist/public/operations/discovery-entities.d.ts.map +1 -1
  54. package/dist/public/operations/discovery-entities.js +28 -0
  55. package/dist/public/operations/discovery-executors.d.ts.map +1 -1
  56. package/dist/public/operations/discovery-executors.js +13 -1
  57. package/dist/public/operations/result-envelope.d.ts +2 -0
  58. package/dist/public/operations/result-envelope.d.ts.map +1 -1
  59. package/dist/public/operations/result-envelope.js +1 -0
  60. package/dist/public/operations/source-relationship-parity.d.ts.map +1 -1
  61. package/dist/public/operations/source-relationship-parity.js +22 -7
  62. package/dist/public/operations/specs/check.d.ts +1 -1
  63. package/dist/public/operations/specs/check.js +1 -1
  64. package/dist/public/skills/kibi-bootstrap/SKILL.md +2 -2
  65. package/dist/public/skills/kibi-usage/SKILL.md +11 -5
  66. package/dist/traceability/git-change-snapshot.d.ts.map +1 -1
  67. package/dist/traceability/git-change-snapshot.js +44 -3
  68. package/dist/traceability/staged-diagnostics.d.ts +1 -1
  69. package/dist/traceability/staged-diagnostics.d.ts.map +1 -1
  70. package/dist/traceability/staged-migration.d.ts +13 -0
  71. package/dist/traceability/staged-migration.d.ts.map +1 -0
  72. package/dist/traceability/staged-migration.js +59 -0
  73. package/dist/traceability/staged-symbols-manifest.d.ts +11 -0
  74. package/dist/traceability/staged-symbols-manifest.d.ts.map +1 -1
  75. package/dist/traceability/staged-symbols-manifest.js +50 -16
  76. package/package.json +2 -2
  77. package/src/public/impact/analyzer.ts +21 -7
  78. package/src/public/impact/diagnostics.ts +1 -1
  79. package/src/public/impact/unowned-files.ts +229 -0
  80. package/src/public/operations/discovery-entities.ts +36 -0
  81. package/src/public/operations/discovery-executors.ts +17 -1
  82. package/src/public/operations/result-envelope.ts +3 -0
  83. package/src/public/operations/source-relationship-parity.ts +22 -7
  84. package/src/public/operations/specs/check.ts +1 -1
  85. package/src/public/skills/kibi-bootstrap/SKILL.md +2 -2
  86. 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.18.0
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: find the governing requirement with `kb_search` and
116
- `sourceLocations`, then link a symbol to it with `implements` through
117
- `kb_upsert`. Without a readable manifest (`Kibi: the hook could not read
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;AAsED;;;;;;;;;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,CAsB3E;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"}
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
- for (const rename of renamedPaths(root, baseTree, headTree)) {
528
- if (!isKnowledgeRelocation(rename.from, rename.to))
529
- continue;
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;AAKzB,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;CAC5B;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;AAoTD,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;CACpD,GAAG,qCAAqC,CAqExC"}
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
- function getChangedAuthoredEntityIds(headSymbols, stagedSymbols) {
203
- const headSignatures = new Map(headSymbols.map((symbol) => [
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
- JSON.stringify(symbol),
224
+ symbol,
206
225
  ]));
207
- const changedIds = stagedSymbols.flatMap((symbol) => {
226
+ const changed = [];
227
+ const migrationOnly = [];
228
+ for (const symbol of stagedSymbols) {
208
229
  if (symbol.id === null)
209
- return [];
210
- const stagedSignature = JSON.stringify(symbol);
211
- return headSignatures.get(getAuthoredSymbolComparisonKey(symbol)) ===
212
- stagedSignature
213
- ? []
214
- : [symbol.id];
215
- });
216
- return uniqueSorted(changedIds);
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
- return [
437
- sourcePath,
438
- getChangedAuthoredEntityIds(headSymbols, stagedSymbols),
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.25.0",
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.1",
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
- activeManifestResults
161
- .filter((result) =>
162
- result.relationships.some((relationship) =>
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 with the change: 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.`
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
+ }