kibi-cli 2.22.1 → 2.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-register-foundation.d.ts.map +1 -1
- package/dist/cli-register-foundation.js +1 -0
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +51 -3
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +28 -0
- package/dist/commands/migrate.d.ts +10 -0
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +58 -9
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +15 -1
- package/dist/extractors/markdown.d.ts.map +1 -1
- package/dist/extractors/markdown.js +37 -35
- package/dist/intent-search.d.ts.map +1 -1
- package/dist/intent-search.js +32 -4
- package/dist/operations/bootstrap/activation.d.ts +7 -1
- package/dist/operations/bootstrap/activation.d.ts.map +1 -1
- package/dist/operations/bootstrap/activation.js +27 -2
- package/dist/operations/bootstrap/generate.d.ts.map +1 -1
- package/dist/operations/bootstrap/generate.js +56 -16
- package/dist/operations/bootstrap/guidance-actions.d.ts.map +1 -1
- package/dist/operations/bootstrap/guidance-actions.js +14 -2
- package/dist/operations/bootstrap/guidance.d.ts.map +1 -1
- package/dist/operations/bootstrap/guidance.js +6 -2
- package/dist/operations/bootstrap/presentation.d.ts.map +1 -1
- package/dist/operations/bootstrap/presentation.js +75 -9
- package/dist/operations/bootstrap/types.d.ts +6 -0
- package/dist/operations/bootstrap/types.d.ts.map +1 -1
- package/dist/operations/migration/kb-sources.d.ts +8 -5
- package/dist/operations/migration/kb-sources.d.ts.map +1 -1
- package/dist/operations/migration/kb-sources.js +50 -14
- package/dist/operations/migration/schema6.d.ts.map +1 -1
- package/dist/operations/migration/schema6.js +11 -2
- package/dist/operations/migration/source-paths.d.ts +18 -1
- package/dist/operations/migration/source-paths.d.ts.map +1 -1
- package/dist/operations/migration/source-paths.js +111 -16
- package/dist/operations/modeling/suggest-predicates.d.ts.map +1 -1
- package/dist/operations/modeling/suggest-predicates.js +22 -7
- package/dist/operations/mutation/requirement-semantics.d.ts +16 -0
- package/dist/operations/mutation/requirement-semantics.d.ts.map +1 -1
- package/dist/operations/mutation/requirement-semantics.js +50 -0
- package/dist/operations/mutation/strict-fact.js +1 -1
- package/dist/operations/mutation/upsert.d.ts.map +1 -1
- package/dist/operations/mutation/upsert.js +2 -1
- package/dist/operations/planning/apply-plan.d.ts.map +1 -1
- package/dist/operations/planning/apply-plan.js +22 -2
- package/dist/operations/semantic-advisor/analyze-prose.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/analyze-prose.js +49 -17
- package/dist/public/impact/analyzer.d.ts.map +1 -1
- package/dist/public/impact/analyzer.js +7 -1
- package/dist/public/impact/diagnostics.d.ts.map +1 -1
- package/dist/public/impact/diagnostics.js +16 -3
- package/dist/public/impact/types.d.ts +2 -0
- package/dist/public/impact/types.d.ts.map +1 -1
- package/dist/public/operations/catalog.d.ts +7 -2
- package/dist/public/operations/catalog.d.ts.map +1 -1
- package/dist/public/operations/check-executor.d.ts.map +1 -1
- package/dist/public/operations/check-executor.js +10 -3
- package/dist/public/operations/check-helpers.d.ts +10 -0
- package/dist/public/operations/check-helpers.d.ts.map +1 -1
- package/dist/public/operations/check-helpers.js +54 -0
- package/dist/public/operations/contracts.d.ts.map +1 -1
- package/dist/public/operations/contracts.js +1 -0
- package/dist/public/operations/discovery-executors.d.ts +18 -0
- package/dist/public/operations/discovery-executors.d.ts.map +1 -1
- package/dist/public/operations/discovery-executors.js +62 -3
- package/dist/public/operations/schema6-check-actions.d.ts +4 -0
- package/dist/public/operations/schema6-check-actions.d.ts.map +1 -1
- package/dist/public/operations/schema6-check-actions.js +39 -2
- package/dist/public/operations/source-path-dangling.d.ts +1 -1
- package/dist/public/operations/source-path-dangling.d.ts.map +1 -1
- package/dist/public/operations/source-path-dangling.js +11 -2
- package/dist/public/operations/specs/composite.d.ts +1 -1
- package/dist/public/operations/specs/discovery.d.ts +5 -0
- package/dist/public/operations/specs/discovery.d.ts.map +1 -1
- package/dist/public/operations/specs/discovery.js +5 -0
- package/dist/public/operations/specs/planning.d.ts +1 -1
- package/dist/public/operations/specs/planning.js +1 -1
- package/dist/public/operations/specs/reporting.d.ts.map +1 -1
- package/dist/public/operations/specs/reporting.js +6 -4
- package/dist/public/operations/specs/semantic.d.ts +1 -1
- package/dist/public/operations/specs/semantic.d.ts.map +1 -1
- package/dist/public/operations/specs/semantic.js +4 -2
- package/dist/public/skills/kibi-bootstrap/SKILL.md +3 -2
- package/dist/public/skills/kibi-usage/SKILL.md +6 -3
- package/dist/public/skills/kibi-usage/resources/workflows.md +3 -1
- package/dist/search-answer.d.ts +14 -0
- package/dist/search-answer.d.ts.map +1 -1
- package/dist/search-answer.js +42 -4
- package/dist/traceability/git-change-snapshot.d.ts +46 -0
- package/dist/traceability/git-change-snapshot.d.ts.map +1 -1
- package/dist/traceability/git-change-snapshot.js +187 -3
- package/dist/traceability/staged-diagnostics.d.ts.map +1 -1
- package/dist/traceability/staged-diagnostics.js +5 -6
- package/dist/traceability/staged-inventory-drift.d.ts +24 -0
- package/dist/traceability/staged-inventory-drift.d.ts.map +1 -0
- package/dist/traceability/staged-inventory-drift.js +84 -0
- package/dist/traceability/staged-symbols-manifest.d.ts.map +1 -1
- package/dist/traceability/staged-symbols-manifest.js +1 -2
- package/dist/utils/content-freshness.d.ts +9 -0
- package/dist/utils/content-freshness.d.ts.map +1 -1
- package/dist/utils/content-freshness.js +33 -0
- package/dist/utils/merge-driver-setup.d.ts +10 -0
- package/dist/utils/merge-driver-setup.d.ts.map +1 -0
- package/dist/utils/merge-driver-setup.js +69 -0
- package/dist/utils/rule-registry.generated.d.ts +1 -1
- package/dist/utils/rule-registry.generated.js +1 -1
- package/package.json +2 -2
- package/src/public/impact/analyzer.ts +13 -1
- package/src/public/impact/diagnostics.ts +20 -3
- package/src/public/impact/types.ts +2 -0
- package/src/public/operations/check-executor.ts +16 -4
- package/src/public/operations/check-helpers.ts +65 -0
- package/src/public/operations/contracts.ts +1 -0
- package/src/public/operations/discovery-executors.ts +97 -3
- package/src/public/operations/schema6-check-actions.ts +43 -2
- package/src/public/operations/source-path-dangling.ts +12 -2
- package/src/public/operations/specs/discovery.ts +6 -0
- package/src/public/operations/specs/planning.ts +1 -1
- package/src/public/operations/specs/reporting.ts +6 -4
- package/src/public/operations/specs/semantic.ts +4 -2
- package/src/public/skills/kibi-bootstrap/SKILL.md +3 -2
- package/src/public/skills/kibi-usage/SKILL.md +6 -3
- package/src/public/skills/kibi-usage/resources/workflows.md +3 -1
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { ENTITY_LANES, isEntityLanePath, isKbKnowledgePath, isSymbolsManifestPath, } from "../utils/kb-paths.js";
|
|
3
6
|
const GIT_EXEC_MAX_BUFFER = 64 * 1024 * 1024;
|
|
4
7
|
function runGit(root, args) {
|
|
5
8
|
try {
|
|
@@ -368,12 +371,188 @@ function snapshotFromTrees(root, baseTree, headTree, headCommit, readGit, assert
|
|
|
368
371
|
inventory: buildInventory(root, baseTree, headTree),
|
|
369
372
|
};
|
|
370
373
|
}
|
|
374
|
+
/** The commits named by MERGE_HEAD while a merge commit is being made. */
|
|
375
|
+
function pendingMergeParents(root) {
|
|
376
|
+
let mergeHeadPath;
|
|
377
|
+
try {
|
|
378
|
+
mergeHeadPath = gitText(root, ["rev-parse", "--git-path", "MERGE_HEAD"]);
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
return [];
|
|
382
|
+
}
|
|
383
|
+
let text;
|
|
384
|
+
try {
|
|
385
|
+
text = readFileSync(path.resolve(root, mergeHeadPath), "utf8");
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
return [];
|
|
389
|
+
}
|
|
390
|
+
return text
|
|
391
|
+
.split(/\r?\n/)
|
|
392
|
+
.map((line) => line.trim())
|
|
393
|
+
.filter((line) => /^[a-f0-9]{40,64}$/.test(line));
|
|
394
|
+
}
|
|
395
|
+
function changedPaths(root, from, to) {
|
|
396
|
+
return runGit(root, ["diff", "--name-only", "-z", "--no-renames", from, to])
|
|
397
|
+
.toString("utf8")
|
|
398
|
+
.split("\0")
|
|
399
|
+
.filter(Boolean);
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* For a staged merge commit, the tree the merge is judged against: HEAD
|
|
403
|
+
* with every path the merge takes unchanged from a merged parent replaced
|
|
404
|
+
* by that staged content. Diffing it against the staged tree leaves only
|
|
405
|
+
* what the merge itself wrote, so debt inherited from the merged branch is
|
|
406
|
+
* not charged to the merge. Null when no merge is in progress or nothing
|
|
407
|
+
* is inherited.
|
|
408
|
+
*/
|
|
409
|
+
// implements REQ-014
|
|
410
|
+
export function mergeResolutionBase(root, baseTree, headTree, parents) {
|
|
411
|
+
if (parents.length === 0)
|
|
412
|
+
return null;
|
|
413
|
+
const changed = changedPaths(root, baseTree, headTree);
|
|
414
|
+
if (changed.length === 0)
|
|
415
|
+
return null;
|
|
416
|
+
const inherited = new Set();
|
|
417
|
+
for (const parent of parents) {
|
|
418
|
+
const parentTree = treeForCommit(root, parent);
|
|
419
|
+
const differs = new Set(changedPaths(root, parentTree, headTree));
|
|
420
|
+
for (const file of changed) {
|
|
421
|
+
if (!differs.has(file))
|
|
422
|
+
inherited.add(file);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (inherited.size === 0)
|
|
426
|
+
return null;
|
|
427
|
+
const paths = [...inherited].sort();
|
|
428
|
+
const listed = runGit(root, [
|
|
429
|
+
"ls-tree",
|
|
430
|
+
"-r",
|
|
431
|
+
"-z",
|
|
432
|
+
"--full-tree",
|
|
433
|
+
headTree,
|
|
434
|
+
"--",
|
|
435
|
+
...paths.map(literalPathspec),
|
|
436
|
+
])
|
|
437
|
+
.toString("utf8")
|
|
438
|
+
.split("\0")
|
|
439
|
+
.filter(Boolean);
|
|
440
|
+
const present = new Set();
|
|
441
|
+
const entries = [];
|
|
442
|
+
for (const row of listed) {
|
|
443
|
+
const tab = row.indexOf("\t");
|
|
444
|
+
if (tab < 0)
|
|
445
|
+
continue;
|
|
446
|
+
const [mode, , oid] = row.slice(0, tab).split(" ");
|
|
447
|
+
const file = row.slice(tab + 1);
|
|
448
|
+
present.add(file);
|
|
449
|
+
entries.push(`${mode} ${oid}\t${file}`);
|
|
450
|
+
}
|
|
451
|
+
const zero = "0".repeat(baseTree.length);
|
|
452
|
+
for (const file of paths) {
|
|
453
|
+
// Deleted on the merged side and absent from the staged tree.
|
|
454
|
+
if (!present.has(file))
|
|
455
|
+
entries.push(`0 ${zero}\t${file}`);
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
baseTree: writeTreeWithEntries(root, baseTree, entries, "kibi-merge-base-"),
|
|
459
|
+
inherited: paths,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
/** Write `baseTree` with `--index-info` entries applied in a scratch index. */
|
|
463
|
+
function writeTreeWithEntries(root, baseTree, entries, prefix) {
|
|
464
|
+
const scratch = mkdtempSync(path.join(tmpdir(), prefix));
|
|
465
|
+
const env = { ...process.env, GIT_INDEX_FILE: path.join(scratch, "index") };
|
|
466
|
+
try {
|
|
467
|
+
const git = (args, input) => execFileSync("git", [...args], {
|
|
468
|
+
cwd: root,
|
|
469
|
+
env,
|
|
470
|
+
maxBuffer: GIT_EXEC_MAX_BUFFER,
|
|
471
|
+
...(input !== undefined ? { input } : {}),
|
|
472
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
473
|
+
});
|
|
474
|
+
git(["read-tree", baseTree]);
|
|
475
|
+
git(["update-index", "-z", "--index-info"], `${entries.join("\0")}\0`);
|
|
476
|
+
return git(["write-tree"]).toString("utf8").trim();
|
|
477
|
+
}
|
|
478
|
+
finally {
|
|
479
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
function renamedPaths(root, from, to) {
|
|
483
|
+
return parseNameStatus(runGit(root, [
|
|
484
|
+
"diff",
|
|
485
|
+
"--name-status",
|
|
486
|
+
"-z",
|
|
487
|
+
"-M",
|
|
488
|
+
"--diff-filter=R",
|
|
489
|
+
from,
|
|
490
|
+
to,
|
|
491
|
+
])).flatMap((row) => row.status === "R" && row.oldPath !== undefined
|
|
492
|
+
? [{ from: row.oldPath, to: row.path }]
|
|
493
|
+
: []);
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* True when `from -> to` moves one knowledge file from outside the canonical
|
|
497
|
+
* layout into the same kind of canonical `.kb/` location: an entity lane
|
|
498
|
+
* file into the same lane, or a symbols manifest / coordinates file onto the
|
|
499
|
+
* canonical file of the same name (`.yml` and `.yaml` alike).
|
|
500
|
+
*/
|
|
501
|
+
function isKnowledgeRelocation(from, to) {
|
|
502
|
+
if (isKbKnowledgePath(from) || !isKbKnowledgePath(to))
|
|
503
|
+
return false;
|
|
504
|
+
if (isSymbolsManifestPath(to)) {
|
|
505
|
+
const stem = (file) => path.posix.basename(file).replace(/\.ya?ml$/, "");
|
|
506
|
+
return stem(from) === stem(to);
|
|
507
|
+
}
|
|
508
|
+
const lane = to.split("/")[1] ?? "";
|
|
509
|
+
return (ENTITY_LANES.includes(lane) &&
|
|
510
|
+
`/${from}`.includes(`/${lane}/`));
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* For a staged snapshot, the tree a commit that relocates knowledge files
|
|
514
|
+
* into `.kb/` is judged against: `baseTree` with each relocated file's
|
|
515
|
+
* committed content placed at its new path and the old path removed.
|
|
516
|
+
* Diffing it against the staged tree leaves only the edits the commit made
|
|
517
|
+
* inside those files, so findings that already existed at the old path are
|
|
518
|
+
* not charged to the move. Every other change, including an edit to a file
|
|
519
|
+
* that was already canonical, is diffed as before. Null when nothing is
|
|
520
|
+
* relocated.
|
|
521
|
+
*/
|
|
522
|
+
// implements REQ-014
|
|
523
|
+
export function renameResolutionBase(root, baseTree, headTree) {
|
|
524
|
+
const zero = "0".repeat(baseTree.length);
|
|
525
|
+
const renames = [];
|
|
526
|
+
const entries = [];
|
|
527
|
+
for (const rename of renamedPaths(root, baseTree, headTree)) {
|
|
528
|
+
if (!isKnowledgeRelocation(rename.from, rename.to))
|
|
529
|
+
continue;
|
|
530
|
+
const source = readTreeEntry(root, baseTree, rename.from);
|
|
531
|
+
if (source?.type !== "blob")
|
|
532
|
+
continue;
|
|
533
|
+
renames.push(rename);
|
|
534
|
+
entries.push(`0 ${zero}\t${rename.from}`);
|
|
535
|
+
entries.push(`${source.mode} ${source.oid}\t${rename.to}`);
|
|
536
|
+
}
|
|
537
|
+
if (renames.length === 0)
|
|
538
|
+
return null;
|
|
539
|
+
return {
|
|
540
|
+
baseTree: writeTreeWithEntries(root, baseTree, entries, "kibi-rename-base-"),
|
|
541
|
+
renames: renames.sort((left, right) => left.to.localeCompare(right.to)),
|
|
542
|
+
};
|
|
543
|
+
}
|
|
371
544
|
/** Capture one immutable tree from the index and inventory only Git objects. */
|
|
372
545
|
// implements REQ-014
|
|
373
546
|
export function captureStagedSnapshot(workspaceRoot) {
|
|
374
547
|
const headCommit = resolveHeadCommit(workspaceRoot);
|
|
375
|
-
const baseTree = treeForCommit(workspaceRoot, headCommit);
|
|
376
548
|
const headTree = gitText(workspaceRoot, ["write-tree"]);
|
|
549
|
+
const commitTree = treeForCommit(workspaceRoot, headCommit);
|
|
550
|
+
const merge = headCommit
|
|
551
|
+
? mergeResolutionBase(workspaceRoot, commitTree, headTree, pendingMergeParents(workspaceRoot))
|
|
552
|
+
: null;
|
|
553
|
+
const mergeTree = merge?.baseTree ?? commitTree;
|
|
554
|
+
const relocation = renameResolutionBase(workspaceRoot, mergeTree, headTree);
|
|
555
|
+
const baseTree = relocation?.baseTree ?? mergeTree;
|
|
377
556
|
const readGit = (args) => translateReadGit(workspaceRoot, args, baseTree, headTree, headCommit);
|
|
378
557
|
const assertUnchanged = () => {
|
|
379
558
|
const currentHead = resolveHeadCommit(workspaceRoot);
|
|
@@ -382,7 +561,12 @@ export function captureStagedSnapshot(workspaceRoot) {
|
|
|
382
561
|
throw new Error("Git index or HEAD changed during staged analysis; retry the operation");
|
|
383
562
|
}
|
|
384
563
|
};
|
|
385
|
-
|
|
564
|
+
const snapshot = snapshotFromTrees(workspaceRoot, baseTree, headTree, headCommit, readGit, assertUnchanged);
|
|
565
|
+
return {
|
|
566
|
+
...snapshot,
|
|
567
|
+
...(merge ? { mergeInherited: merge.inherited } : {}),
|
|
568
|
+
...(relocation ? { renameResolved: relocation.renames } : {}),
|
|
569
|
+
};
|
|
386
570
|
}
|
|
387
571
|
/**
|
|
388
572
|
* Capture changes between explicit revisions. The caller chooses `base`; for
|
|
@@ -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;
|
|
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"}
|
|
@@ -8,13 +8,12 @@ function deriveCoordinatesPath(symbolsManifestPath) {
|
|
|
8
8
|
function formatFileList(paths) {
|
|
9
9
|
return paths.join(", ");
|
|
10
10
|
}
|
|
11
|
-
/**
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Every name, in order: the gate output is the list a person works through,
|
|
13
|
+
* so it is never cut short.
|
|
14
|
+
*/
|
|
13
15
|
function formatNameList(names) {
|
|
14
|
-
|
|
15
|
-
const overflow = names.length - shown.length;
|
|
16
|
-
const rendered = shown.join(", ");
|
|
17
|
-
return overflow > 0 ? `${rendered} … and ${overflow} more` : rendered;
|
|
16
|
+
return names.join(", ");
|
|
18
17
|
}
|
|
19
18
|
function symbolsManifestFileDetails(evidence, paths) {
|
|
20
19
|
const pathSet = new Set(paths);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ExtractionResult } from "../extractors/markdown.js";
|
|
2
|
+
/** A staged requirement whose semantic inventory no longer matches its prose. */
|
|
3
|
+
export declare const STAGED_INVENTORY_DRIFT_RULE = "semantic-inventory-drift";
|
|
4
|
+
export type StagedInventoryDriftViolation = Readonly<{
|
|
5
|
+
rule: typeof STAGED_INVENTORY_DRIFT_RULE;
|
|
6
|
+
entityId: string;
|
|
7
|
+
source: string;
|
|
8
|
+
message: string;
|
|
9
|
+
errors: readonly string[];
|
|
10
|
+
}>;
|
|
11
|
+
/**
|
|
12
|
+
* The staged requirements that declare a semantic inventory contract (now
|
|
13
|
+
* or in the base tree) whose inventory no longer matches their prose. The
|
|
14
|
+
* next `kibi sync` refuses them under the proposition-complete boundary;
|
|
15
|
+
* the gate applies the same test, so drift is caught at the commit that
|
|
16
|
+
* introduces it instead of after a later merge. Requirements without a
|
|
17
|
+
* contract are left to sync, which decides from its own baseline.
|
|
18
|
+
*/
|
|
19
|
+
export declare function stagedInventoryDriftViolations(input: {
|
|
20
|
+
stagedPaths: ReadonlySet<string>;
|
|
21
|
+
snapshotEntities: readonly ExtractionResult[];
|
|
22
|
+
readBase: (path: string) => string | null;
|
|
23
|
+
}): StagedInventoryDriftViolation[];
|
|
24
|
+
//# sourceMappingURL=staged-inventory-drift.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"staged-inventory-drift.d.ts","sourceRoot":"","sources":["../../src/traceability/staged-inventory-drift.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAKlE,iFAAiF;AAEjF,eAAO,MAAM,2BAA2B,6BAA6B,CAAC;AAGtE,MAAM,MAAM,6BAA6B,GAAG,QAAQ,CAAC;IACnD,IAAI,EAAE,OAAO,2BAA2B,CAAC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3B,CAAC,CAAC;AAgCH;;;;;;;GAOG;AAEH,wBAAgB,8BAA8B,CAAC,KAAK,EAAE;IACpD,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACjC,gBAAgB,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC9C,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;CAC3C,GAAG,6BAA6B,EAAE,CA8BlC"}
|
|
@@ -0,0 +1,84 @@
|
|
|
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 { extractFromMarkdownString } from "../extractors/markdown.js";
|
|
19
|
+
import { analyzeSemanticAdvisorInput } from "../operations/semantic-advisor/analyze-prose.js";
|
|
20
|
+
import { validateSemanticInventoryBoundary } from "../operations/semantic-advisor/ingestion-boundary.js";
|
|
21
|
+
/** A staged requirement whose semantic inventory no longer matches its prose. */
|
|
22
|
+
// implements REQ-kibi-proposition-complete-ingestion
|
|
23
|
+
export const STAGED_INVENTORY_DRIFT_RULE = "semantic-inventory-drift";
|
|
24
|
+
function advertisesContract(entity) {
|
|
25
|
+
return (entity.semantic_inventory_version !== undefined ||
|
|
26
|
+
entity.semantic_source_hash !== undefined ||
|
|
27
|
+
entity.semantic_inventory !== undefined);
|
|
28
|
+
}
|
|
29
|
+
function payloadOf(result) {
|
|
30
|
+
return {
|
|
31
|
+
type: result.entity.type,
|
|
32
|
+
id: result.entity.id,
|
|
33
|
+
properties: result.entity,
|
|
34
|
+
relationships: result.relationships,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function baseResult(path, readBase) {
|
|
38
|
+
const content = readBase(path);
|
|
39
|
+
if (content === null)
|
|
40
|
+
return null;
|
|
41
|
+
try {
|
|
42
|
+
return extractFromMarkdownString(content, path);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The staged requirements that declare a semantic inventory contract (now
|
|
50
|
+
* or in the base tree) whose inventory no longer matches their prose. The
|
|
51
|
+
* next `kibi sync` refuses them under the proposition-complete boundary;
|
|
52
|
+
* the gate applies the same test, so drift is caught at the commit that
|
|
53
|
+
* introduces it instead of after a later merge. Requirements without a
|
|
54
|
+
* contract are left to sync, which decides from its own baseline.
|
|
55
|
+
*/
|
|
56
|
+
// implements REQ-kibi-proposition-complete-ingestion
|
|
57
|
+
export function stagedInventoryDriftViolations(input) {
|
|
58
|
+
const violations = [];
|
|
59
|
+
for (const result of input.snapshotEntities) {
|
|
60
|
+
if (result.entity.type !== "req")
|
|
61
|
+
continue;
|
|
62
|
+
const source = result.entity.source;
|
|
63
|
+
if (!input.stagedPaths.has(source))
|
|
64
|
+
continue;
|
|
65
|
+
const payload = payloadOf(result);
|
|
66
|
+
const semantic = analyzeSemanticAdvisorInput({ payload });
|
|
67
|
+
const boundary = validateSemanticInventoryBoundary(payload, result.relationships, semantic.receipt);
|
|
68
|
+
if (boundary.errors.length === 0)
|
|
69
|
+
continue;
|
|
70
|
+
const base = baseResult(source, input.readBase);
|
|
71
|
+
const enforce = advertisesContract(result.entity) ||
|
|
72
|
+
(base !== null && advertisesContract(base.entity));
|
|
73
|
+
if (!enforce)
|
|
74
|
+
continue;
|
|
75
|
+
violations.push({
|
|
76
|
+
rule: STAGED_INVENTORY_DRIFT_RULE,
|
|
77
|
+
entityId: result.entity.id,
|
|
78
|
+
source,
|
|
79
|
+
errors: boundary.errors,
|
|
80
|
+
message: `${result.entity.id} (${source}): the semantic inventory does not match the requirement's prose, so the next kibi sync rejects it: ${boundary.errors.join("; ")}. Run 'kibi migrate' to re-derive it, or kb_model mode analyze with the complete prose and keep its inventory contract.`,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return violations.sort((left, right) => left.entityId.localeCompare(right.entityId));
|
|
84
|
+
}
|
|
@@ -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;
|
|
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"}
|
|
@@ -281,8 +281,7 @@ function diffSymbolEvidence(sourcePath, expectedSymbols, stagedSymbols) {
|
|
|
281
281
|
if (missing.length === 0 && extra.length === 0) {
|
|
282
282
|
return null;
|
|
283
283
|
}
|
|
284
|
-
// Keep the complete lists:
|
|
285
|
-
// accurate "… and N more"), while JSON evidence carries every name.
|
|
284
|
+
// Keep the complete lists: the gate output names every symbol.
|
|
286
285
|
return {
|
|
287
286
|
path: sourcePath,
|
|
288
287
|
expectedCount: expectedSymbols.length,
|
|
@@ -9,5 +9,14 @@ export declare function confirmSourceNewerReasons(workspaceRoot: string, storePa
|
|
|
9
9
|
kept: Reason[];
|
|
10
10
|
dropped: Reason[];
|
|
11
11
|
};
|
|
12
|
+
/**
|
|
13
|
+
* The `meta` freshness block reporting operations return (kb_graph,
|
|
14
|
+
* kb_find_gaps, kb_coverage) comes straight from Prolog, which compares file
|
|
15
|
+
* times only. Apply the same content confirmation kb_status does, so a
|
|
16
|
+
* checkout or `touch` that changed no byte is not reported as stale.
|
|
17
|
+
*/
|
|
18
|
+
export declare function confirmMetaFreshness<T extends {
|
|
19
|
+
readonly meta?: Readonly<Record<string, unknown>>;
|
|
20
|
+
}>(payload: T, workspaceRoot: string): T;
|
|
12
21
|
export {};
|
|
13
22
|
//# sourceMappingURL=content-freshness.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content-freshness.d.ts","sourceRoot":"","sources":["../../src/utils/content-freshness.ts"],"names":[],"mappings":"AAYA,uDAAuD;AACvD,eAAO,MAAM,yBAAyB,EAAE,WAAW,CAAC,MAAM,CAIxD,CAAC;AAEH,KAAK,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAyJhD;;;GAGG;AAEH,wBAAgB,yBAAyB,CACvC,aAAa,EAAE,MAAM,EACrB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,SAAS,MAAM,EAAE,GACzB;IAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAkBvC"}
|
|
1
|
+
{"version":3,"file":"content-freshness.d.ts","sourceRoot":"","sources":["../../src/utils/content-freshness.ts"],"names":[],"mappings":"AAYA,uDAAuD;AACvD,eAAO,MAAM,yBAAyB,EAAE,WAAW,CAAC,MAAM,CAIxD,CAAC;AAEH,KAAK,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAyJhD;;;GAGG;AAEH,wBAAgB,yBAAyB,CACvC,aAAa,EAAE,MAAM,EACrB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,SAAS,MAAM,EAAE,GACzB;IAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAkBvC;AAED;;;;;GAKG;AAEH,wBAAgB,oBAAoB,CAClC,CAAC,SAAS;IAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;CAAE,EAC/D,OAAO,EAAE,CAAC,EAAE,aAAa,EAAE,MAAM,GAAG,CAAC,CAoCtC"}
|
|
@@ -169,3 +169,36 @@ export function confirmSourceNewerReasons(workspaceRoot, storePath, reasons) {
|
|
|
169
169
|
}
|
|
170
170
|
return { kept, dropped };
|
|
171
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* The `meta` freshness block reporting operations return (kb_graph,
|
|
174
|
+
* kb_find_gaps, kb_coverage) comes straight from Prolog, which compares file
|
|
175
|
+
* times only. Apply the same content confirmation kb_status does, so a
|
|
176
|
+
* checkout or `touch` that changed no byte is not reported as stale.
|
|
177
|
+
*/
|
|
178
|
+
// implements REQ-cli-status-pre-first-sync
|
|
179
|
+
export function confirmMetaFreshness(payload, workspaceRoot) {
|
|
180
|
+
const meta = payload.meta;
|
|
181
|
+
if (meta === undefined ||
|
|
182
|
+
typeof meta.kbPath !== "string" ||
|
|
183
|
+
!Array.isArray(meta.staleReasons))
|
|
184
|
+
return payload;
|
|
185
|
+
const reasons = meta.staleReasons.filter((reason) => record(reason) !== null);
|
|
186
|
+
const { kept, dropped } = confirmSourceNewerReasons(workspaceRoot, meta.kbPath, reasons);
|
|
187
|
+
if (dropped.length === 0)
|
|
188
|
+
return payload;
|
|
189
|
+
const allConfirmed = kept.length === 0 &&
|
|
190
|
+
meta.staleReasonsTruncated !== true &&
|
|
191
|
+
meta.syncState === "stale";
|
|
192
|
+
return {
|
|
193
|
+
...payload,
|
|
194
|
+
meta: {
|
|
195
|
+
...meta,
|
|
196
|
+
staleReasons: kept,
|
|
197
|
+
// A truncated list counts more reasons than it carries.
|
|
198
|
+
staleReasonCount: Math.max(kept.length, (typeof meta.staleReasonCount === "number"
|
|
199
|
+
? meta.staleReasonCount
|
|
200
|
+
: reasons.length) - dropped.length),
|
|
201
|
+
...(allConfirmed ? { syncState: "fresh", dirty: false } : {}),
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** How to route the authored manifests to `kibi merge-driver`. */
|
|
2
|
+
export declare const MERGE_DRIVER_SETUP = "Add to .gitattributes: \".kb/symbols.yaml merge=kibi\" and \".kb/relationships/*.yaml merge=kibi\"; then run: git config merge.kibi.driver \"npm exec --no -- kibi merge-driver %O %A %B\" (see docs/cli-reference.md#kibi-merge-driver-base-current-other).";
|
|
3
|
+
/**
|
|
4
|
+
* What keeps Git from resolving `.kb/symbols.yaml` with `kibi merge-driver`:
|
|
5
|
+
* the attribute that routes the file to it, and the driver command. Empty
|
|
6
|
+
* when both are set, and null outside a Git work tree or while the
|
|
7
|
+
* manifest holds no symbol records (nothing to conflict on yet).
|
|
8
|
+
*/
|
|
9
|
+
export declare function manifestMergeDriverGaps(cwd: string): string[] | null;
|
|
10
|
+
//# sourceMappingURL=merge-driver-setup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge-driver-setup.d.ts","sourceRoot":"","sources":["../../src/utils/merge-driver-setup.ts"],"names":[],"mappings":"AAsBA,kEAAkE;AAElE,eAAO,MAAM,kBAAkB,iQAC2N,CAAC;AAwB3P;;;;;GAKG;AAEH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAiBpE"}
|
|
@@ -0,0 +1,69 @@
|
|
|
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 { execFileSync } from "node:child_process";
|
|
19
|
+
import { readFileSync } from "node:fs";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
/** How to route the authored manifests to `kibi merge-driver`. */
|
|
22
|
+
// implements REQ-cli-doctor
|
|
23
|
+
export const MERGE_DRIVER_SETUP = 'Add to .gitattributes: ".kb/symbols.yaml merge=kibi" and ".kb/relationships/*.yaml merge=kibi"; then run: git config merge.kibi.driver "npm exec --no -- kibi merge-driver %O %A %B" (see docs/cli-reference.md#kibi-merge-driver-base-current-other).';
|
|
24
|
+
function gitOutput(cwd, args) {
|
|
25
|
+
try {
|
|
26
|
+
return execFileSync("git", [...args], {
|
|
27
|
+
cwd,
|
|
28
|
+
encoding: "utf8",
|
|
29
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
30
|
+
}).trim();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function hasSymbolRecords(cwd) {
|
|
37
|
+
try {
|
|
38
|
+
return /^\s*-\s+id\s*:/m.test(readFileSync(path.join(cwd, ".kb", "symbols.yaml"), "utf8"));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* What keeps Git from resolving `.kb/symbols.yaml` with `kibi merge-driver`:
|
|
46
|
+
* the attribute that routes the file to it, and the driver command. Empty
|
|
47
|
+
* when both are set, and null outside a Git work tree or while the
|
|
48
|
+
* manifest holds no symbol records (nothing to conflict on yet).
|
|
49
|
+
*/
|
|
50
|
+
// implements REQ-cli-doctor
|
|
51
|
+
export function manifestMergeDriverGaps(cwd) {
|
|
52
|
+
if (gitOutput(cwd, ["rev-parse", "--is-inside-work-tree"]) !== "true") {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
if (!hasSymbolRecords(cwd))
|
|
56
|
+
return null;
|
|
57
|
+
const attribute = gitOutput(cwd, [
|
|
58
|
+
"check-attr",
|
|
59
|
+
"merge",
|
|
60
|
+
"--",
|
|
61
|
+
".kb/symbols.yaml",
|
|
62
|
+
]);
|
|
63
|
+
const routed = attribute?.endsWith(": merge: kibi") === true;
|
|
64
|
+
const driver = gitOutput(cwd, ["config", "--get", "merge.kibi.driver"]);
|
|
65
|
+
return [
|
|
66
|
+
...(routed ? [] : [".kb/symbols.yaml is not routed to merge=kibi"]),
|
|
67
|
+
...(driver ? [] : ["merge.kibi.driver is not set"]),
|
|
68
|
+
];
|
|
69
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export declare const GENERATED_RULES: readonly [{
|
|
2
2
|
readonly name: "must-priority-coverage";
|
|
3
|
-
readonly description: "Every must-priority requirement must have a scenario and a test";
|
|
3
|
+
readonly description: "Every current must-priority requirement (not superseded, retired, or closed without the implemented tag) must have a scenario and a test";
|
|
4
4
|
readonly enforcementClass: "canonical";
|
|
5
5
|
readonly category: "coverage";
|
|
6
6
|
}, {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
export const GENERATED_RULES = [
|
|
11
11
|
{
|
|
12
12
|
name: "must-priority-coverage",
|
|
13
|
-
description: "Every must-priority requirement must have a scenario and a test",
|
|
13
|
+
description: "Every current must-priority requirement (not superseded, retired, or closed without the implemented tag) must have a scenario and a test",
|
|
14
14
|
enforcementClass: "canonical",
|
|
15
15
|
category: "coverage",
|
|
16
16
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.23.0",
|
|
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.1",
|
|
239
239
|
"kibi-plugin-builtin": "^0.5.2",
|
|
240
240
|
"kibi-plugin-sdk": "^0.5.0",
|
|
241
241
|
"kibi-swipl": "^1.0.1",
|
|
@@ -11,6 +11,7 @@ import { captureStagedSnapshot } from "../../traceability/git-change-snapshot.js
|
|
|
11
11
|
import { readSnapshotKnowledge } from "../../traceability/snapshot-knowledge.js";
|
|
12
12
|
import type { ExtractedSymbol } from "../../traceability/symbol-extract.js";
|
|
13
13
|
import { extractSymbolsFromStagedFileAsync } from "../../traceability/symbol-extract.js";
|
|
14
|
+
import { isTraceabilityRelationshipType } from "../symbol-granularity.js";
|
|
14
15
|
import {
|
|
15
16
|
createSemanticReviewDiagnostics,
|
|
16
17
|
createSymbolGranularityDiagnostics,
|
|
@@ -153,7 +154,18 @@ export async function analyzeChangedFileImpact(
|
|
|
153
154
|
manifestResults: activeManifestResults,
|
|
154
155
|
symbolsByFile,
|
|
155
156
|
}),
|
|
156
|
-
...createSemanticReviewDiagnostics({
|
|
157
|
+
...createSemanticReviewDiagnostics({
|
|
158
|
+
symbolsByFile,
|
|
159
|
+
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),
|
|
167
|
+
),
|
|
168
|
+
}),
|
|
157
169
|
];
|
|
158
170
|
const maxDiagnostics = options.maxDiagnostics;
|
|
159
171
|
const cappedDiagnostics =
|
|
@@ -81,6 +81,12 @@ function getGranularSymbolsForSourceFile(
|
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
function hasTraceabilityLinks(symbol: ExtractedSymbol): boolean {
|
|
85
|
+
return (symbol.relationships ?? []).some((relationship) =>
|
|
86
|
+
isTraceabilityRelationshipType(relationship.type),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
84
90
|
function formatRelationshipTargets(symbol: ExtractedSymbol): string {
|
|
85
91
|
const targets = [
|
|
86
92
|
...new Set(
|
|
@@ -183,15 +189,26 @@ export function createSemanticReviewDiagnostics(
|
|
|
183
189
|
if (!isChangedBehavioralReviewCandidate(symbol)) continue;
|
|
184
190
|
|
|
185
191
|
const linkedTargets = formatRelationshipTargets(symbol);
|
|
192
|
+
const file = symbol.location.file;
|
|
193
|
+
// In a file no requirement owns at all, the changed behavior has no
|
|
194
|
+
// recorded intent, so the review is "record it", not "compare it with
|
|
195
|
+
// its links". Elsewhere a coarser symbol of the file may still own it.
|
|
196
|
+
const unlinked =
|
|
197
|
+
!hasTraceabilityLinks(symbol) &&
|
|
198
|
+
!(options.linkedSourceFiles?.has(file) ?? false);
|
|
186
199
|
diagnostics.push({
|
|
187
200
|
id: "symbol_semantic_review_needed",
|
|
188
201
|
severity: "warning",
|
|
189
202
|
blocking: false,
|
|
190
203
|
category: "symbol",
|
|
191
|
-
files: [
|
|
204
|
+
files: [file],
|
|
192
205
|
docs: ["docs/symbol-traceability-taxonomy.md"],
|
|
193
|
-
message:
|
|
194
|
-
|
|
206
|
+
message: unlinked
|
|
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
|
+
: `Changed behavioral symbol ${symbol.name} needs semantic review against linked Kibi coverage: ${linkedTargets}`,
|
|
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.`
|
|
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.`,
|
|
195
212
|
});
|
|
196
213
|
}
|
|
197
214
|
}
|
|
@@ -49,6 +49,8 @@ export type SymbolGranularityDiagnosticsOptions = {
|
|
|
49
49
|
|
|
50
50
|
export type SemanticReviewDiagnosticsOptions = {
|
|
51
51
|
readonly symbolsByFile: SymbolsByFile;
|
|
52
|
+
/** Source files where some manifest symbol carries a traceability link. */
|
|
53
|
+
readonly linkedSourceFiles?: ReadonlySet<string>;
|
|
52
54
|
};
|
|
53
55
|
|
|
54
56
|
export type SymbolQualityDiagnosticsOptions = {
|