kibi-cli 2.24.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/firm-decision-edit.d.ts +30 -0
- package/dist/firm-decision-edit.d.ts.map +1 -0
- package/dist/firm-decision-edit.js +90 -0
- 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/firm-decision-guard.d.ts +10 -0
- package/dist/operations/mutation/firm-decision-guard.d.ts.map +1 -0
- package/dist/operations/mutation/firm-decision-guard.js +71 -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 +34 -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 +3 -3
- package/dist/public/operations/decision-firmness.d.ts +11 -1
- package/dist/public/operations/decision-firmness.d.ts.map +1 -1
- package/dist/public/operations/decision-firmness.js +78 -5
- 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-rules.generated.d.ts +1 -1
- package/dist/public/operations/specs/check-rules.generated.d.ts.map +1 -1
- package/dist/public/operations/specs/check-rules.generated.js +2 -1
- package/dist/public/operations/specs/check.d.ts +2 -2
- package/dist/public/operations/specs/check.js +1 -1
- package/dist/public/operations/specs/mutation-schemas.d.ts +1 -1
- package/dist/public/operations/specs/mutation-schemas.js +1 -1
- package/dist/public/operations/specs/upsert.d.ts +1 -1
- package/dist/public/skills/kibi-bootstrap/SKILL.md +2 -2
- package/dist/public/skills/kibi-usage/SKILL.md +18 -7
- 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/dist/utils/rule-registry.generated.d.ts +5 -0
- package/dist/utils/rule-registry.generated.d.ts.map +1 -1
- package/dist/utils/rule-registry.generated.js +6 -0
- 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/decision-firmness.ts +110 -13
- 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-rules.generated.ts +2 -1
- package/src/public/operations/specs/check.ts +1 -1
- package/src/public/operations/specs/mutation-schemas.ts +1 -1
- package/src/public/skills/kibi-bootstrap/SKILL.md +2 -2
- package/src/public/skills/kibi-usage/SKILL.md +18 -7
|
@@ -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
|
}
|
|
@@ -243,6 +243,11 @@ export declare const GENERATED_RULES: readonly [{
|
|
|
243
243
|
readonly description: "A requirement or ADR that supersedes a firm decision, retired or not, must itself be firm (and so carry a ref to the human who changed the rule); a default or exploratory replacement cannot overrule a firm decision";
|
|
244
244
|
readonly enforcementClass: "canonical";
|
|
245
245
|
readonly category: "lifecycle";
|
|
246
|
+
}, {
|
|
247
|
+
readonly name: "decision-firm-edited";
|
|
248
|
+
readonly description: "A current requirement or ADR that was firm at HEAD must not have its claim (title, stated meaning, the statement its body derives, an ADR's body, or its firmness) changed in place unless the edit cites new human words (a changed origin.ref, or approval_ref on a requirement); supersede it with a firm decision instead";
|
|
249
|
+
readonly enforcementClass: "canonical";
|
|
250
|
+
readonly category: "lifecycle";
|
|
246
251
|
}, {
|
|
247
252
|
readonly name: "supersedes-reason-missing";
|
|
248
253
|
readonly description: "A current requirement or ADR that supersedes another must explain why under a 'Why this changed' heading (at least 12 words of prose naming the evidence and answering the old decision's reasoning); supersede edges recorded before schema 9 are acknowledged in the manifest and exempt";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rule-registry.generated.d.ts","sourceRoot":"","sources":["../../src/utils/rule-registry.generated.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,eAAe
|
|
1
|
+
{"version":3,"file":"rule-registry.generated.d.ts","sourceRoot":"","sources":["../../src/utils/rule-registry.generated.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuXlB,CAAC"}
|
|
@@ -301,6 +301,12 @@ export const GENERATED_RULES = [
|
|
|
301
301
|
enforcementClass: "canonical",
|
|
302
302
|
category: "lifecycle",
|
|
303
303
|
},
|
|
304
|
+
{
|
|
305
|
+
name: "decision-firm-edited",
|
|
306
|
+
description: "A current requirement or ADR that was firm at HEAD must not have its claim (title, stated meaning, the statement its body derives, an ADR's body, or its firmness) changed in place unless the edit cites new human words (a changed origin.ref, or approval_ref on a requirement); supersede it with a firm decision instead",
|
|
307
|
+
enforcementClass: "canonical",
|
|
308
|
+
category: "lifecycle",
|
|
309
|
+
},
|
|
304
310
|
{
|
|
305
311
|
name: "supersedes-reason-missing",
|
|
306
312
|
description: "A current requirement or ADR that supersedes another must explain why under a 'Why this changed' heading (at least 12 words of prose naming the evidence and answering the old decision's reasoning); supersede edges recorded before schema 9 are acknowledged in the manifest and exempt",
|
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
|
+
}
|