kibi-cli 2.20.0 → 2.21.2
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-operation-metadata.d.ts +1 -1
- package/dist/cli-operation-metadata.js +1 -1
- package/dist/commands/doctor.js +13 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +15 -3
- package/dist/commands/legacy-storage-migration.d.ts.map +1 -1
- package/dist/commands/legacy-storage-migration.js +26 -1
- package/dist/commands/migrate.d.ts +2 -0
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +155 -11
- package/dist/commands/sync/discovery.d.ts.map +1 -1
- package/dist/commands/sync/discovery.js +14 -2
- package/dist/commands/sync/persistence.d.ts.map +1 -1
- package/dist/commands/sync/persistence.js +4 -0
- package/dist/decision-firmness.d.ts +39 -0
- package/dist/decision-firmness.d.ts.map +1 -0
- package/dist/decision-firmness.js +115 -0
- package/dist/extractors/markdown.d.ts +2 -0
- package/dist/extractors/markdown.d.ts.map +1 -1
- package/dist/extractors/markdown.js +16 -1
- package/dist/operations/migration/kb-sources.d.ts +5 -1
- package/dist/operations/migration/kb-sources.d.ts.map +1 -1
- package/dist/operations/migration/kb-sources.js +28 -1
- package/dist/operations/migration/schema6.d.ts +2 -0
- package/dist/operations/migration/schema6.d.ts.map +1 -1
- package/dist/operations/migration/schema6.js +44 -1
- package/dist/operations/migration/supersedes-reason.d.ts +26 -0
- package/dist/operations/migration/supersedes-reason.d.ts.map +1 -0
- package/dist/operations/migration/supersedes-reason.js +69 -0
- package/dist/operations/modeling/predicate-applyplan.d.ts.map +1 -1
- package/dist/operations/modeling/predicate-applyplan.js +10 -2
- package/dist/operations/modeling/predicate-loader.d.ts.map +1 -1
- package/dist/operations/modeling/predicate-loader.js +11 -0
- package/dist/operations/modeling/predicate-types.d.ts +32 -1
- package/dist/operations/modeling/predicate-types.d.ts.map +1 -1
- package/dist/operations/modeling/suggest-predicates.d.ts.map +1 -1
- package/dist/operations/modeling/suggest-predicates.js +175 -35
- package/dist/operations/mutation/delete.d.ts +5 -1
- package/dist/operations/mutation/delete.d.ts.map +1 -1
- package/dist/operations/mutation/delete.js +223 -17
- package/dist/operations/mutation/serialization.d.ts.map +1 -1
- package/dist/operations/mutation/serialization.js +1 -0
- package/dist/operations/mutation/source-authoring.d.ts +8 -0
- package/dist/operations/mutation/source-authoring.d.ts.map +1 -1
- package/dist/operations/mutation/source-authoring.js +33 -0
- package/dist/operations/mutation/types.d.ts +37 -0
- package/dist/operations/mutation/types.d.ts.map +1 -1
- package/dist/operations/mutation/upsert.d.ts.map +1 -1
- package/dist/operations/mutation/upsert.js +54 -2
- package/dist/operations/mutation/validation.d.ts.map +1 -1
- package/dist/operations/mutation/validation.js +2 -1
- package/dist/operations/planning/apply-plan.d.ts +6 -1
- package/dist/operations/planning/apply-plan.d.ts.map +1 -1
- package/dist/operations/planning/apply-plan.js +8 -2
- package/dist/operations/semantic-advisor/clauses.d.ts +9 -0
- package/dist/operations/semantic-advisor/clauses.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/clauses.js +24 -0
- package/dist/public/operations/catalog.d.ts +14 -5
- 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 +3 -0
- package/dist/public/operations/contracts.d.ts.map +1 -1
- package/dist/public/operations/contracts.js +29 -1
- package/dist/public/operations/decision-firmness.d.ts +34 -0
- package/dist/public/operations/decision-firmness.d.ts.map +1 -0
- package/dist/public/operations/decision-firmness.js +216 -0
- package/dist/public/operations/discovery-executors.d.ts.map +1 -1
- package/dist/public/operations/discovery-executors.js +16 -2
- package/dist/public/operations/migration-plan.d.ts.map +1 -1
- package/dist/public/operations/migration-plan.js +37 -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 +5 -1
- package/dist/public/operations/specs/check.d.ts +1 -1
- package/dist/public/operations/specs/mutation-schemas.d.ts +7 -2
- package/dist/public/operations/specs/mutation-schemas.d.ts.map +1 -1
- package/dist/public/operations/specs/mutation-schemas.js +13 -17
- package/dist/public/operations/specs/mutation.d.ts +6 -2
- package/dist/public/operations/specs/mutation.d.ts.map +1 -1
- package/dist/public/operations/specs/mutation.js +6 -2
- package/dist/public/operations/specs/upsert.d.ts +7 -2
- package/dist/public/operations/specs/upsert.d.ts.map +1 -1
- package/dist/public/schemas/entity.d.ts.map +1 -1
- package/dist/public/schemas/entity.js +13 -13
- package/dist/public/skills/kibi-traceability/SKILL.md +6 -4
- package/dist/public/skills/kibi-usage/SKILL.md +52 -10
- package/dist/public/skills/kibi-usage/resources/fact-lanes.md +1 -1
- package/dist/public/skills/kibi-usage/resources/source-authoring.md +4 -2
- package/dist/public/skills/kibi-usage/resources/ui-requirements.md +2 -2
- package/dist/public/skills/kibi-usage/resources/workflows.md +1 -1
- package/dist/relationships/shards.d.ts +10 -0
- package/dist/relationships/shards.d.ts.map +1 -1
- package/dist/relationships/shards.js +27 -11
- package/dist/schemas/entity.schema.json +30 -25
- package/dist/search-answer.d.ts +31 -2
- package/dist/search-answer.d.ts.map +1 -1
- package/dist/search-answer.js +115 -17
- package/dist/types/entities.d.ts +4 -0
- package/dist/types/entities.d.ts.map +1 -1
- package/dist/utils/kb-manifest.d.ts +6 -0
- package/dist/utils/kb-manifest.d.ts.map +1 -1
- package/dist/utils/kb-manifest.js +6 -0
- package/dist/utils/rule-registry.generated.d.ts +21 -0
- package/dist/utils/rule-registry.generated.d.ts.map +1 -1
- package/dist/utils/rule-registry.generated.js +25 -0
- package/dist/utils/schema-version.d.ts +1 -1
- package/dist/utils/schema-version.d.ts.map +1 -1
- package/dist/utils/schema-version.js +3 -1
- package/package.json +2 -2
- package/schema/entities.pl +6 -0
- package/src/public/operations/check-executor.ts +6 -0
- package/src/public/operations/contracts.ts +33 -1
- package/src/public/operations/decision-firmness.ts +280 -0
- package/src/public/operations/discovery-executors.ts +17 -2
- package/src/public/operations/migration-plan.ts +41 -9
- package/src/public/operations/specs/check-rules.generated.ts +5 -1
- package/src/public/operations/specs/mutation-schemas.ts +14 -17
- package/src/public/operations/specs/mutation.ts +7 -2
- package/src/public/schemas/entity.ts +14 -13
- package/src/public/skills/kibi-traceability/SKILL.md +6 -4
- package/src/public/skills/kibi-usage/SKILL.md +52 -10
- package/src/public/skills/kibi-usage/resources/fact-lanes.md +1 -1
- package/src/public/skills/kibi-usage/resources/source-authoring.md +4 -2
- package/src/public/skills/kibi-usage/resources/ui-requirements.md +2 -2
- package/src/public/skills/kibi-usage/resources/workflows.md +1 -1
- package/src/schemas/entity.schema.json +30 -25
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
stagedEntityIdStyleDiagnostics,
|
|
42
42
|
} from "./check-helpers.js";
|
|
43
43
|
import { collectCheckPolicyViolations } from "./check-policy-rules.js";
|
|
44
|
+
import { collectDecisionFirmnessViolations } from "./decision-firmness.js";
|
|
44
45
|
import { executeStatus } from "./discovery-executors.js";
|
|
45
46
|
import { collectEntityContextViolations } from "./entity-context.js";
|
|
46
47
|
import {
|
|
@@ -375,6 +376,10 @@ export async function executeCheck(
|
|
|
375
376
|
rulesAllowlist,
|
|
376
377
|
workspaceRoot,
|
|
377
378
|
);
|
|
379
|
+
const decisionFirmnessFindings = collectDecisionFirmnessViolations(
|
|
380
|
+
rulesAllowlist,
|
|
381
|
+
workspaceRoot,
|
|
382
|
+
);
|
|
378
383
|
const checkPolicyFindings = await collectCheckPolicyViolations(
|
|
379
384
|
prolog,
|
|
380
385
|
rulesAllowlist,
|
|
@@ -388,6 +393,7 @@ export async function executeCheck(
|
|
|
388
393
|
...predicateConformanceFindings,
|
|
389
394
|
...originReviewFindings,
|
|
390
395
|
...entityContextFindings,
|
|
396
|
+
...decisionFirmnessFindings,
|
|
391
397
|
...checkPolicyFindings,
|
|
392
398
|
]);
|
|
393
399
|
const violations: Violation[] = partitioned.violations;
|
|
@@ -569,6 +569,8 @@ const BASE_DATA_SCHEMAS: Readonly<
|
|
|
569
569
|
"record_ontology_gap",
|
|
570
570
|
"review_nonlogical",
|
|
571
571
|
"replace_grounding",
|
|
572
|
+
"extend_requirement",
|
|
573
|
+
"keep_existing_grounding",
|
|
572
574
|
],
|
|
573
575
|
},
|
|
574
576
|
recommendedPredicateSchema: { type: ["object", "null"] },
|
|
@@ -584,7 +586,16 @@ const BASE_DATA_SCHEMAS: Readonly<
|
|
|
584
586
|
description:
|
|
585
587
|
"The planned predicate fact id (FACT-PRED-...) that a requires_predicate link must target. Candidate ids (SUGGEST-...) are never relationship targets.",
|
|
586
588
|
},
|
|
587
|
-
existingGrounding:
|
|
589
|
+
existingGrounding: {
|
|
590
|
+
...recordArray,
|
|
591
|
+
description:
|
|
592
|
+
"The grounding links requirementId already has for this claim: matched by claim_key (the same proposition text) or, for a predicate fact, by the recommended candidate's canonical_key (the same predicate tuple in other words; matchedBy says which).",
|
|
593
|
+
},
|
|
594
|
+
decisionConflicts: {
|
|
595
|
+
...recordArray,
|
|
596
|
+
description:
|
|
597
|
+
"When no hash-bound plan ran the contradiction analysis: stored decision facts of current requirements that agree with the complete candidate on its key arguments and decide the opposite (requirementId, factId, effect, proposedEffect, canonicalKey). A warning repeats each one.",
|
|
598
|
+
},
|
|
588
599
|
replacementPlan: { type: ["object", "null"] },
|
|
589
600
|
bindingHints: {
|
|
590
601
|
...recordArray,
|
|
@@ -648,6 +659,11 @@ const BASE_DATA_SCHEMAS: Readonly<
|
|
|
648
659
|
relationships_created: integerValue,
|
|
649
660
|
warnings: valueArray,
|
|
650
661
|
semanticAdvisor: recordValue,
|
|
662
|
+
claimKey: {
|
|
663
|
+
...stringValue,
|
|
664
|
+
description:
|
|
665
|
+
"For a fact with claim_text: the stored claim_key, derived from claim_text when the payload did not supply it.",
|
|
666
|
+
},
|
|
651
667
|
status: stringValue,
|
|
652
668
|
effectFailures: recordArray,
|
|
653
669
|
nextActions: recordArray,
|
|
@@ -685,6 +701,22 @@ const BASE_DATA_SCHEMAS: Readonly<
|
|
|
685
701
|
status: stringValue,
|
|
686
702
|
effectFailures: recordArray,
|
|
687
703
|
nextActions: recordArray,
|
|
704
|
+
outcome: {
|
|
705
|
+
type: "string",
|
|
706
|
+
enum: [
|
|
707
|
+
"plan_returned",
|
|
708
|
+
"supersession_required",
|
|
709
|
+
"plan_applied",
|
|
710
|
+
"dry_run",
|
|
711
|
+
],
|
|
712
|
+
description:
|
|
713
|
+
"Absent on a direct delete. plan_returned: deletionPlan was returned and nothing was written; nextActions[0].input is the exact kb_apply_plan input. supersession_required: an authored requirement must be superseded with kb_upsert instead. plan_applied: approvedPlanHash matched and the plan was applied. dry_run: dryRun:true previewed the delete and nothing was written.",
|
|
714
|
+
},
|
|
715
|
+
dryRun: { const: true },
|
|
716
|
+
wouldDelete: recordValue,
|
|
717
|
+
planHash: stringValue,
|
|
718
|
+
sourcePaths: stringArray,
|
|
719
|
+
recoveryJournalId: stringValue,
|
|
688
720
|
}),
|
|
689
721
|
kb_check: {
|
|
690
722
|
description:
|
|
@@ -0,0 +1,280 @@
|
|
|
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
|
+
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import {
|
|
21
|
+
MIN_WHY_CHANGED_WORDS,
|
|
22
|
+
WHY_THIS_CHANGED_HEADING,
|
|
23
|
+
carriesFirmness,
|
|
24
|
+
effectiveFirmness,
|
|
25
|
+
firmnessRef,
|
|
26
|
+
supersedeAcknowledgement,
|
|
27
|
+
whyThisChangedWords,
|
|
28
|
+
} from "../../decision-firmness.js";
|
|
29
|
+
import {
|
|
30
|
+
listLaneMarkdownFiles,
|
|
31
|
+
readText,
|
|
32
|
+
} from "../../operations/migration/kb-sources.js";
|
|
33
|
+
import { readAllShards } from "../../relationships/shards.js";
|
|
34
|
+
import { readKbManifest } from "../../utils/kb-manifest.js";
|
|
35
|
+
import type { Violation } from "../../utils/rule-registry.js";
|
|
36
|
+
import {
|
|
37
|
+
type AuthoredEntity,
|
|
38
|
+
isCurrentAuthoredEntity,
|
|
39
|
+
readAuthoredEntity,
|
|
40
|
+
} from "./entity-context.js";
|
|
41
|
+
|
|
42
|
+
/** A firm decision must cite the human words it rests on (blocking). */
|
|
43
|
+
// implements REQ-kb-decision-firmness
|
|
44
|
+
export const DECISION_FIRM_UNREFERENCED_RULE = "decision-firm-unreferenced";
|
|
45
|
+
/** Only a firm decision may supersede a firm decision (blocking). */
|
|
46
|
+
// implements REQ-kb-decision-firmness
|
|
47
|
+
export const DECISION_FIRM_SUPERSEDED_RULE = "decision-firm-superseded";
|
|
48
|
+
/** A supersede must say why under "Why this changed" (blocking). */
|
|
49
|
+
// implements REQ-kb-decision-firmness
|
|
50
|
+
export const SUPERSEDES_REASON_MISSING_RULE = "supersedes-reason-missing";
|
|
51
|
+
/** How many legacy supersedes the schema 9 migration acknowledged (info). */
|
|
52
|
+
// implements REQ-kb-decision-firmness
|
|
53
|
+
export const SUPERSEDES_REASON_ACKNOWLEDGED_RULE =
|
|
54
|
+
"supersedes-reason-acknowledged";
|
|
55
|
+
|
|
56
|
+
// implements REQ-kb-decision-firmness
|
|
57
|
+
export const DECISION_FIRMNESS_RULES = [
|
|
58
|
+
DECISION_FIRM_UNREFERENCED_RULE,
|
|
59
|
+
DECISION_FIRM_SUPERSEDED_RULE,
|
|
60
|
+
SUPERSEDES_REASON_MISSING_RULE,
|
|
61
|
+
SUPERSEDES_REASON_ACKNOWLEDGED_RULE,
|
|
62
|
+
] as const;
|
|
63
|
+
|
|
64
|
+
const DECISION_LANES = ["requirements", "adr"] as const;
|
|
65
|
+
|
|
66
|
+
/** Every authored requirement and ADR. */
|
|
67
|
+
// implements REQ-kb-decision-firmness
|
|
68
|
+
export function listDecisionEntities(workspaceRoot: string): AuthoredEntity[] {
|
|
69
|
+
const entities: AuthoredEntity[] = [];
|
|
70
|
+
for (const file of listLaneMarkdownFiles(workspaceRoot, DECISION_LANES)) {
|
|
71
|
+
const content = readText(file.absolutePath);
|
|
72
|
+
if (content === null) continue;
|
|
73
|
+
const entity = readAuthoredEntity(content, file.relativePath);
|
|
74
|
+
if (entity !== null && carriesFirmness(entity.type)) entities.push(entity);
|
|
75
|
+
}
|
|
76
|
+
return entities;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Supersedes edges from requirements and ADRs, keyed by the superseding id,
|
|
81
|
+
* read from authored `links` and the relationship shards so the check and the
|
|
82
|
+
* migration agree before a sync. A shard edge whose source entity no longer
|
|
83
|
+
* exists is ignored: it must not make its target look replaced.
|
|
84
|
+
*/
|
|
85
|
+
// implements REQ-kb-decision-firmness
|
|
86
|
+
export function authoredSupersedes(
|
|
87
|
+
workspaceRoot: string,
|
|
88
|
+
entities: readonly AuthoredEntity[],
|
|
89
|
+
): Map<string, Set<string>> {
|
|
90
|
+
const edges = new Map<string, Set<string>>();
|
|
91
|
+
const known = new Set(entities.map((entity) => entity.id));
|
|
92
|
+
const add = (from: string, to: string) => {
|
|
93
|
+
const targets = edges.get(from) ?? new Set<string>();
|
|
94
|
+
targets.add(to);
|
|
95
|
+
edges.set(from, targets);
|
|
96
|
+
};
|
|
97
|
+
for (const entity of entities) {
|
|
98
|
+
const links = entity.data.links;
|
|
99
|
+
if (!Array.isArray(links)) continue;
|
|
100
|
+
for (const link of links) {
|
|
101
|
+
if (
|
|
102
|
+
link !== null &&
|
|
103
|
+
typeof link === "object" &&
|
|
104
|
+
(link as { type?: unknown }).type === "supersedes" &&
|
|
105
|
+
typeof (link as { target?: unknown }).target === "string"
|
|
106
|
+
) {
|
|
107
|
+
add(entity.id, (link as { target: string }).target);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
for (const relationship of readAllShards(path.join(workspaceRoot, ".kb"))) {
|
|
113
|
+
if (relationship.type === "supersedes" && known.has(relationship.from))
|
|
114
|
+
add(relationship.from, relationship.to);
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
// Malformed shards fail sync with their own diagnostic.
|
|
118
|
+
}
|
|
119
|
+
return edges;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function supersededIds(edges: ReadonlyMap<string, Set<string>>): Set<string> {
|
|
123
|
+
const superseded = new Set<string>();
|
|
124
|
+
for (const targets of edges.values())
|
|
125
|
+
for (const target of targets) superseded.add(target);
|
|
126
|
+
return superseded;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Superseding requirements and ADRs, whatever their status, without a "Why
|
|
131
|
+
* this changed" explanation. The schema 9 migration acknowledges their edges
|
|
132
|
+
* once, retired ones included, so one that becomes current again does not
|
|
133
|
+
* start blocking; read-only.
|
|
134
|
+
*/
|
|
135
|
+
// implements REQ-kb-decision-firmness
|
|
136
|
+
export function unexplainedSupersedes(
|
|
137
|
+
entities: readonly AuthoredEntity[],
|
|
138
|
+
edges: ReadonlyMap<string, Set<string>>,
|
|
139
|
+
): AuthoredEntity[] {
|
|
140
|
+
return entities
|
|
141
|
+
.filter(
|
|
142
|
+
(entity) =>
|
|
143
|
+
edges.has(entity.id) &&
|
|
144
|
+
whyThisChangedWords(entity.body) < MIN_WHY_CHANGED_WORDS,
|
|
145
|
+
)
|
|
146
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The supersede edges of an entity, as the manifest acknowledges them. */
|
|
150
|
+
// implements REQ-kb-decision-firmness
|
|
151
|
+
export function supersedeAcknowledgements(
|
|
152
|
+
id: string,
|
|
153
|
+
edges: ReadonlyMap<string, Set<string>>,
|
|
154
|
+
): string[] {
|
|
155
|
+
return [...(edges.get(id) ?? [])]
|
|
156
|
+
.sort()
|
|
157
|
+
.map((target) => supersedeAcknowledgement(id, target));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Evaluate the firmness rules over authored requirements and ADRs. */
|
|
161
|
+
// implements REQ-kb-decision-firmness
|
|
162
|
+
export function evaluateDecisionFirmness(
|
|
163
|
+
entities: readonly AuthoredEntity[],
|
|
164
|
+
edges: ReadonlyMap<string, Set<string>>,
|
|
165
|
+
rules: ReadonlySet<string> = new Set(DECISION_FIRMNESS_RULES),
|
|
166
|
+
acknowledged: ReadonlySet<string> = new Set(),
|
|
167
|
+
): Violation[] {
|
|
168
|
+
const findings: Violation[] = [];
|
|
169
|
+
const superseded = supersededIds(edges);
|
|
170
|
+
const byId = new Map(entities.map((entity) => [entity.id, entity]));
|
|
171
|
+
const current = entities
|
|
172
|
+
.filter((entity) => isCurrentAuthoredEntity(entity, superseded))
|
|
173
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
174
|
+
const firmnessOf = (entity: AuthoredEntity) =>
|
|
175
|
+
effectiveFirmness(entity.type, entity.data.firmness);
|
|
176
|
+
|
|
177
|
+
if (rules.has(DECISION_FIRM_UNREFERENCED_RULE)) {
|
|
178
|
+
for (const entity of current) {
|
|
179
|
+
if (firmnessOf(entity) !== "firm") continue;
|
|
180
|
+
if (firmnessRef({ type: entity.type, ...entity.data }) !== undefined)
|
|
181
|
+
continue;
|
|
182
|
+
findings.push({
|
|
183
|
+
rule: DECISION_FIRM_UNREFERENCED_RULE,
|
|
184
|
+
entityId: entity.id,
|
|
185
|
+
description: `${entity.id} is marked firmness: firm but names no human words it rests on (origin.ref${entity.type === "req" ? " or approval_ref" : ""} is missing)`,
|
|
186
|
+
suggestion:
|
|
187
|
+
"Set origin.ref to the message, review or ticket where a human stated this rule; if no human stated it, use firmness: default (or exploratory for a guess)",
|
|
188
|
+
source: entity.path,
|
|
189
|
+
evidence: { type: entity.type, firmness: "firm" },
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Every superseder counts, retired ones too: replacing a non-firm
|
|
195
|
+
// superseder must not launder the overruled firm decision.
|
|
196
|
+
if (rules.has(DECISION_FIRM_SUPERSEDED_RULE)) {
|
|
197
|
+
for (const entity of [...entities].sort((left, right) =>
|
|
198
|
+
left.id.localeCompare(right.id),
|
|
199
|
+
)) {
|
|
200
|
+
if (firmnessOf(entity) === "firm") continue;
|
|
201
|
+
const firmTargets = [...(edges.get(entity.id) ?? [])]
|
|
202
|
+
.filter((id) => {
|
|
203
|
+
const target = byId.get(id);
|
|
204
|
+
return (
|
|
205
|
+
target !== undefined &&
|
|
206
|
+
effectiveFirmness(target.type, target.data.firmness) === "firm"
|
|
207
|
+
);
|
|
208
|
+
})
|
|
209
|
+
.sort();
|
|
210
|
+
if (firmTargets.length === 0) continue;
|
|
211
|
+
findings.push({
|
|
212
|
+
rule: DECISION_FIRM_SUPERSEDED_RULE,
|
|
213
|
+
entityId: entity.id,
|
|
214
|
+
description: `${entity.id} (firmness: ${firmnessOf(entity)}) supersedes the firm decision${firmTargets.length === 1 ? "" : "s"} ${firmTargets.join(", ")}; only a human may change a firm decision`,
|
|
215
|
+
suggestion:
|
|
216
|
+
"Ask the person who stated the firm decision; if they agree, mark the replacement firmness: firm with origin.ref to their words, otherwise remove the supersedes link and propose the change instead",
|
|
217
|
+
source: entity.path,
|
|
218
|
+
evidence: { firmness: firmnessOf(entity), firmTargets },
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const currentIds = new Set(current.map((entity) => entity.id));
|
|
224
|
+
const unexplained = unexplainedSupersedes(entities, edges).filter((entity) =>
|
|
225
|
+
currentIds.has(entity.id),
|
|
226
|
+
);
|
|
227
|
+
const isAcknowledged = (entity: AuthoredEntity) =>
|
|
228
|
+
supersedeAcknowledgements(entity.id, edges).every((edge) =>
|
|
229
|
+
acknowledged.has(edge),
|
|
230
|
+
);
|
|
231
|
+
const exempt = unexplained.filter(isAcknowledged);
|
|
232
|
+
if (rules.has(SUPERSEDES_REASON_MISSING_RULE)) {
|
|
233
|
+
for (const entity of unexplained) {
|
|
234
|
+
if (isAcknowledged(entity)) continue;
|
|
235
|
+
const targets = [...(edges.get(entity.id) ?? [])].sort();
|
|
236
|
+
const unacknowledged = targets.filter(
|
|
237
|
+
(target) =>
|
|
238
|
+
!acknowledged.has(supersedeAcknowledgement(entity.id, target)),
|
|
239
|
+
);
|
|
240
|
+
findings.push({
|
|
241
|
+
rule: SUPERSEDES_REASON_MISSING_RULE,
|
|
242
|
+
entityId: entity.id,
|
|
243
|
+
description: `${entity.id} supersedes ${unacknowledged.join(", ")} without a '## ${WHY_THIS_CHANGED_HEADING}' section of at least ${MIN_WHY_CHANGED_WORDS} words`,
|
|
244
|
+
suggestion: `Add '## ${WHY_THIS_CHANGED_HEADING}' to the body: name the evidence (a failing test, a measured cost, a constraint the old decision missed, a new requirement) and answer the superseded decision's reasoning. A preference is not evidence; propose it instead of superseding.`,
|
|
245
|
+
source: entity.path,
|
|
246
|
+
evidence: { type: entity.type, supersedes: unacknowledged },
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (rules.has(SUPERSEDES_REASON_ACKNOWLEDGED_RULE) && exempt.length) {
|
|
251
|
+
findings.push({
|
|
252
|
+
rule: SUPERSEDES_REASON_ACKNOWLEDGED_RULE,
|
|
253
|
+
entityId: "workspace",
|
|
254
|
+
description: `${exempt.length} current superseding requirement${exempt.length === 1 ? " or ADR was" : "s and ADRs were"} recorded before schema 9 without a '${WHY_THIS_CHANGED_HEADING}' section`,
|
|
255
|
+
suggestion:
|
|
256
|
+
"When you next touch one, add the reason if the history or a person states it; never invent one",
|
|
257
|
+
evidence: {
|
|
258
|
+
total: exempt.length,
|
|
259
|
+
ids: exempt.slice(0, 25).map((entity) => entity.id),
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return findings;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Read the authored decisions once and evaluate the selected rules. */
|
|
267
|
+
// implements REQ-kb-decision-firmness
|
|
268
|
+
export function collectDecisionFirmnessViolations(
|
|
269
|
+
rules: ReadonlySet<string>,
|
|
270
|
+
workspaceRoot: string,
|
|
271
|
+
): Violation[] {
|
|
272
|
+
if (!DECISION_FIRMNESS_RULES.some((rule) => rules.has(rule))) return [];
|
|
273
|
+
const entities = listDecisionEntities(workspaceRoot);
|
|
274
|
+
return evaluateDecisionFirmness(
|
|
275
|
+
entities,
|
|
276
|
+
authoredSupersedes(workspaceRoot, entities),
|
|
277
|
+
rules,
|
|
278
|
+
new Set(readKbManifest(workspaceRoot)?.supersedesReasonAcknowledged ?? []),
|
|
279
|
+
);
|
|
280
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { compilerFingerprintReason } from "../../commands/sync/cache.js";
|
|
3
|
+
import { effectiveFirmness } from "../../decision-firmness.js";
|
|
3
4
|
import { EngineClient } from "../../engine.js";
|
|
4
5
|
import {
|
|
5
6
|
type IntentSearchAnalysis,
|
|
@@ -514,7 +515,13 @@ function factRowSummary(row: Record<string, unknown>): string {
|
|
|
514
515
|
const names = Array.isArray(row.argumentNames)
|
|
515
516
|
? row.argumentNames.map(String).join(", ")
|
|
516
517
|
: "";
|
|
517
|
-
|
|
518
|
+
// A built-in schema is no stored entity: its id is for kb_model's
|
|
519
|
+
// schemaId, not for a kb_query by id.
|
|
520
|
+
const builtin =
|
|
521
|
+
row.builtin === true
|
|
522
|
+
? ` [builtin${id ? ` ${id}` : ""}, not a stored entity]`
|
|
523
|
+
: "";
|
|
524
|
+
return `${row.name}(${names})${builtin}`;
|
|
518
525
|
}
|
|
519
526
|
return `${id} (${String(row.title ?? "")})`;
|
|
520
527
|
}
|
|
@@ -695,7 +702,15 @@ export async function executeQuery(
|
|
|
695
702
|
const indexedPage = prolog.queryEntities
|
|
696
703
|
? await prolog.queryEntities(pageInput, signal)
|
|
697
704
|
: await queryEntitiesViaQuery(prolog, pageInput, signal);
|
|
698
|
-
|
|
705
|
+
// A requirement or ADR without authored firmness reads as `default`;
|
|
706
|
+
// the row says so, so a reader never has to know the default.
|
|
707
|
+
// implements REQ-kb-decision-firmness
|
|
708
|
+
const paginated = indexedPage.entities.map((entity) => {
|
|
709
|
+
const firmness = effectiveFirmness(entity.type, entity.firmness);
|
|
710
|
+
return firmness === undefined || entity.firmness === firmness
|
|
711
|
+
? entity
|
|
712
|
+
: { ...entity, firmness };
|
|
713
|
+
});
|
|
699
714
|
const text =
|
|
700
715
|
indexedPage.count === 0
|
|
701
716
|
? `No entities found${type ? ` of type '${type}'` : ""}.`
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import { planLegacyStorageMigration } from "../../commands/legacy-storage-migration.js";
|
|
4
5
|
import { BRANCH_STORE_NOT_COMPILED } from "../../utils/branch-store.js";
|
|
5
6
|
import { legacyConfigExists } from "../../utils/config.js";
|
|
6
7
|
import { MTIME_SOURCE_REASON_CODES } from "../../utils/content-freshness.js";
|
|
@@ -688,6 +689,14 @@ export function buildActionsFromStatus(input: {
|
|
|
688
689
|
const legacyStorageMigrationRequired = legacyConfigExists(
|
|
689
690
|
input.workspaceRoot,
|
|
690
691
|
);
|
|
692
|
+
// A legacy cutover that cannot move its files (a destination already
|
|
693
|
+
// exists, a malformed config) is an operator decision, not an automatic
|
|
694
|
+
// step that fails when applied and takes the compile with it.
|
|
695
|
+
const legacyStorage = legacyStorageMigrationRequired
|
|
696
|
+
? planLegacyStorageMigration(input.workspaceRoot)
|
|
697
|
+
: null;
|
|
698
|
+
const legacyStorageBlocked =
|
|
699
|
+
legacyStorage !== null && legacyStorage.blockers.length > 0;
|
|
691
700
|
if (storeState === "missing") {
|
|
692
701
|
actions.push(
|
|
693
702
|
migrationAction({
|
|
@@ -724,6 +733,8 @@ export function buildActionsFromStatus(input: {
|
|
|
724
733
|
}
|
|
725
734
|
|
|
726
735
|
if (configStatus.needsMigration || legacyStorageMigrationRequired) {
|
|
736
|
+
const operatorOnly =
|
|
737
|
+
configStatus.status === "newer" || legacyStorageBlocked;
|
|
727
738
|
actions.push(
|
|
728
739
|
migrationAction({
|
|
729
740
|
id: "schema-config-upgrade",
|
|
@@ -734,17 +745,34 @@ export function buildActionsFromStatus(input: {
|
|
|
734
745
|
? "legacy_storage_migration"
|
|
735
746
|
: "schema_version_upgrade",
|
|
736
747
|
category: "schema",
|
|
737
|
-
state:
|
|
738
|
-
safety:
|
|
739
|
-
invocation:
|
|
748
|
+
state: operatorOnly ? "blocked" : "ready",
|
|
749
|
+
safety: operatorOnly ? "operator" : "automatic",
|
|
750
|
+
invocation: legacyStorageBlocked
|
|
751
|
+
? {
|
|
752
|
+
kind: "review",
|
|
753
|
+
instruction: `The legacy .kb/config.json still names knowledge files outside .kb/ and Kibi cannot move them: ${legacyStorage?.blockers.join(" ")} Compare each legacy copy with its .kb/ copy and keep the current one: replace the .kb/ copy when the legacy one is newer, or remove (git rm) the legacy copy when it is a stale duplicate. Then run 'kibi migrate --yes' to retire .kb/config.json.`,
|
|
754
|
+
}
|
|
755
|
+
: { kind: "cli", command_argv: ["kibi", "migrate", "--yes"] },
|
|
740
756
|
affectedFiles: [
|
|
741
757
|
".kb/manifest.json",
|
|
742
758
|
".kb/migrations",
|
|
743
759
|
".kb/config.json",
|
|
760
|
+
...(legacyStorage?.moves.map((move) => move.from) ?? []),
|
|
744
761
|
],
|
|
745
|
-
evidence: {
|
|
762
|
+
evidence: {
|
|
763
|
+
configStatus,
|
|
764
|
+
...(legacyStorage === null
|
|
765
|
+
? {}
|
|
766
|
+
: {
|
|
767
|
+
legacyStorage: {
|
|
768
|
+
moves: legacyStorage.moves,
|
|
769
|
+
blockers: legacyStorage.blockers,
|
|
770
|
+
legacyConfig: legacyStorage.legacyConfig,
|
|
771
|
+
},
|
|
772
|
+
}),
|
|
773
|
+
},
|
|
746
774
|
dependsOn: storeState === "missing" ? ["branch-store-ensure"] : [],
|
|
747
|
-
autoApplicable:
|
|
775
|
+
autoApplicable: !operatorOnly,
|
|
748
776
|
}),
|
|
749
777
|
);
|
|
750
778
|
}
|
|
@@ -769,12 +797,16 @@ export function buildActionsFromStatus(input: {
|
|
|
769
797
|
typeof store?.path === "string" ? store.path : ".kb/branches",
|
|
770
798
|
],
|
|
771
799
|
evidence: { reason: uncompiled },
|
|
800
|
+
// A blocked schema step waits on an operator; the compile still runs
|
|
801
|
+
// from the .kb/ sources Kibi already reads.
|
|
772
802
|
dependsOn: actions
|
|
773
|
-
.map((action) => action.id)
|
|
774
803
|
.filter(
|
|
775
|
-
(
|
|
776
|
-
|
|
777
|
-
|
|
804
|
+
(action) =>
|
|
805
|
+
action.state === "ready" &&
|
|
806
|
+
(action.id === "branch-store-ensure" ||
|
|
807
|
+
action.id === "schema-config-upgrade"),
|
|
808
|
+
)
|
|
809
|
+
.map((action) => action.id),
|
|
778
810
|
postconditions: [{ branchStore: "compiled" }],
|
|
779
811
|
autoApplicable: true,
|
|
780
812
|
}),
|
|
@@ -55,6 +55,10 @@ export const SELECTABLE_RULE_NAMES: readonly string[] = [
|
|
|
55
55
|
"predicate-schema-conformance",
|
|
56
56
|
"entity-context-missing",
|
|
57
57
|
"entity-context-acknowledged",
|
|
58
|
+
"decision-firm-unreferenced",
|
|
59
|
+
"decision-firm-superseded",
|
|
60
|
+
"supersedes-reason-missing",
|
|
61
|
+
"supersedes-reason-acknowledged",
|
|
58
62
|
"policy-ownership",
|
|
59
63
|
"policy-markers",
|
|
60
64
|
];
|
|
@@ -62,4 +66,4 @@ export const SELECTABLE_RULE_NAMES: readonly string[] = [
|
|
|
62
66
|
// implements REQ-006
|
|
63
67
|
/** Comma-separated "Allowed:" fragment for the kb_check rules description. */
|
|
64
68
|
export const SELECTABLE_RULES_ALLOWED =
|
|
65
|
-
"must-priority-coverage, symbol-coverage, symbol-traceability, no-dangling-refs, source-relationship-parity, branch-store-not-compiled, source-path-dangling, no-cycles, required-fields, deprecated-adr-no-successor, superseded-requirement-open, scenario-feasibility, scenario-feasibility-unknown, exception-claim-keys, numeric-string-value, rule-key-arguments-missing, domain-contradictions, logic-coverage, rule-safety, rule-verifiability, query-plan-safety, req-status-vocabulary, strict-fact-shape, strict-req-fact-pairing, predicate-verifiability, strict-readiness, semantic-completeness, related-requirement-unmodeled, proof-contract-symbols, entity-id-style, domain-redundancy, domain-implication, subject-key-identity, subject-key-shape, ontology-quality, exception-unapproved, exception-approval-self-attested, agent-requirement-unapproved, requirement-rationale-missing, symbol-owner-superseded, adr-unlinked, adr-proposed, predicate-schema-conformance, entity-context-missing, entity-context-acknowledged, policy-ownership, policy-markers";
|
|
69
|
+
"must-priority-coverage, symbol-coverage, symbol-traceability, no-dangling-refs, source-relationship-parity, branch-store-not-compiled, source-path-dangling, no-cycles, required-fields, deprecated-adr-no-successor, superseded-requirement-open, scenario-feasibility, scenario-feasibility-unknown, exception-claim-keys, numeric-string-value, rule-key-arguments-missing, domain-contradictions, logic-coverage, rule-safety, rule-verifiability, query-plan-safety, req-status-vocabulary, strict-fact-shape, strict-req-fact-pairing, predicate-verifiability, strict-readiness, semantic-completeness, related-requirement-unmodeled, proof-contract-symbols, entity-id-style, domain-redundancy, domain-implication, subject-key-identity, subject-key-shape, ontology-quality, exception-unapproved, exception-approval-self-attested, agent-requirement-unapproved, requirement-rationale-missing, symbol-owner-superseded, adr-unlinked, adr-proposed, predicate-schema-conformance, entity-context-missing, entity-context-acknowledged, decision-firm-unreferenced, decision-firm-superseded, supersedes-reason-missing, supersedes-reason-acknowledged, policy-ownership, policy-markers";
|
|
@@ -19,22 +19,13 @@ export const ENTITY_TYPES = [
|
|
|
19
19
|
"fact",
|
|
20
20
|
] as const;
|
|
21
21
|
|
|
22
|
-
//
|
|
23
|
-
//
|
|
22
|
+
// A claim_key names the clause in claim_text, so it never travels alone. The
|
|
23
|
+
// reverse is open: Kibi derives a missing claim_key from claim_text on a
|
|
24
|
+
// grounding fact, and an observation or meta fact quotes a claim without one.
|
|
25
|
+
// implements REQ-kibi-review-observation-claim-text-v2
|
|
24
26
|
const CLAIM_PROVENANCE_CONDITIONAL = JSON.parse(`{
|
|
25
|
-
"if": {
|
|
26
|
-
|
|
27
|
-
{ "required": ["claim_key"] },
|
|
28
|
-
{
|
|
29
|
-
"required": ["claim_text"],
|
|
30
|
-
"not": {
|
|
31
|
-
"properties": { "fact_kind": { "enum": ["observation", "meta"] } },
|
|
32
|
-
"required": ["fact_kind"]
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
]
|
|
36
|
-
},
|
|
37
|
-
"then": { "required": ["claim_key", "claim_text"] }
|
|
27
|
+
"if": { "required": ["claim_key"] },
|
|
28
|
+
"then": { "required": ["claim_text"] }
|
|
38
29
|
}`) as Record<string, unknown>;
|
|
39
30
|
|
|
40
31
|
export const ENTITY_PROPERTIES_SCHEMA = {
|
|
@@ -134,6 +125,12 @@ export const ENTITY_PROPERTIES_SCHEMA = {
|
|
|
134
125
|
description:
|
|
135
126
|
"Requirement-only. Optional reference to the exception's approval decision record.",
|
|
136
127
|
},
|
|
128
|
+
firmness: {
|
|
129
|
+
type: "string",
|
|
130
|
+
enum: ["firm", "default", "exploratory"],
|
|
131
|
+
description:
|
|
132
|
+
"req/adr only. firm = a human stated it as a rule (needs origin.ref to their words); default (when absent) = delegated or agent choice; exploratory = flagged guess.",
|
|
133
|
+
},
|
|
137
134
|
rationale: {
|
|
138
135
|
type: "string",
|
|
139
136
|
minLength: 1,
|
|
@@ -293,12 +290,12 @@ export const ENTITY_PROPERTIES_SCHEMA = {
|
|
|
293
290
|
type: "string",
|
|
294
291
|
pattern: "^CLAIM-[A-F0-9]{16}$",
|
|
295
292
|
description:
|
|
296
|
-
"Stable
|
|
293
|
+
"Stable key of the atomic prose clause grounded by this fact, derived from claim_text. Optional: when omitted, Kibi derives it; when sent, it must equal the derived key.",
|
|
297
294
|
},
|
|
298
295
|
claim_text: {
|
|
299
296
|
type: "string",
|
|
300
297
|
description:
|
|
301
|
-
"Exact atomic requirement clause represented by this fact, retained for human audit.
|
|
298
|
+
"Exact atomic requirement clause represented by this fact, retained for human audit. On a grounding fact Kibi derives claim_key from it; an observation or meta fact quotes the clause it is about without a key.",
|
|
302
299
|
},
|
|
303
300
|
predicate_name: {
|
|
304
301
|
type: "string",
|
|
@@ -39,7 +39,7 @@ export const deleteSpec = {
|
|
|
39
39
|
name: "kb_delete",
|
|
40
40
|
cliName: "delete",
|
|
41
41
|
description:
|
|
42
|
-
"Delete entities by ID. Use only for intentional removals after dependency checks. Do not use as a bulk cleanup shortcut. Side effects: mutates and saves KB; skips entities with dependents.",
|
|
42
|
+
"Delete entities by ID, or exact relationship triples. Use only for intentional removals after dependency checks. Do not use as a bulk cleanup shortcut. An entity authored in a Markdown or YAML source is not deleted directly: the call writes nothing and returns outcome plan_returned with a hash-bound deletionPlan and a nextActions entry holding the exact kb_apply_plan input ({plan, approvedPlanHash}); apply that unmodified, or repeat this call with approvedPlanHash = deletionPlan.planHash to apply the freshly computed plan in one step (a stale or different hash fails with DELETE_PLAN_HASH_MISMATCH and writes nothing). Authored requirements are never deleted: outcome supersession_required points to kb_upsert with a supersedes relationship. dryRun:true previews any delete and writes nothing. Side effects: mutates and saves KB and authored sources; skips entities with dependents; dryRun and plan_returned write nothing.",
|
|
43
43
|
businessInputSchema: {
|
|
44
44
|
type: "object",
|
|
45
45
|
oneOf: [
|
|
@@ -73,7 +73,12 @@ export const deleteSpec = {
|
|
|
73
73
|
type: "string",
|
|
74
74
|
pattern: "^[a-fA-F0-9]{64}$",
|
|
75
75
|
description:
|
|
76
|
-
"
|
|
76
|
+
"With ids only: the deletionPlan.planHash a prior call for the same ids returned. The plan is recomputed and applied through kb_apply_plan when its hash still matches; a mismatch fails with DELETE_PLAN_HASH_MISMATCH and writes nothing. Not allowed with relationships or dryRun.",
|
|
77
|
+
},
|
|
78
|
+
dryRun: {
|
|
79
|
+
type: "boolean",
|
|
80
|
+
description:
|
|
81
|
+
"Preview without writing. Relationship and direct entity deletes run their preflight and return outcome dry_run with wouldDelete counts, the sourceWrites (paths and hashes) a commit would make, and skippedEffects. Authored ids return the same deletion plan as without dryRun.",
|
|
77
82
|
},
|
|
78
83
|
},
|
|
79
84
|
},
|
|
@@ -42,19 +42,8 @@ type VerificationPerspective = "internal" | "consumer";
|
|
|
42
42
|
|
|
43
43
|
const factConditionals = JSON.parse(`[
|
|
44
44
|
{
|
|
45
|
-
"if": {
|
|
46
|
-
|
|
47
|
-
{ "required": ["claim_key"] },
|
|
48
|
-
{
|
|
49
|
-
"required": ["claim_text"],
|
|
50
|
-
"not": {
|
|
51
|
-
"properties": { "fact_kind": { "enum": ["observation", "meta"] } },
|
|
52
|
-
"required": ["fact_kind"]
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
]
|
|
56
|
-
},
|
|
57
|
-
"then": { "required": ["claim_key", "claim_text"] }
|
|
45
|
+
"if": { "required": ["claim_key"] },
|
|
46
|
+
"then": { "required": ["claim_text"] }
|
|
58
47
|
},
|
|
59
48
|
{
|
|
60
49
|
"if": {
|
|
@@ -369,6 +358,12 @@ const entitySchema: Record<string, unknown> = {
|
|
|
369
358
|
text_ref: { type: "string" },
|
|
370
359
|
// Provenance of any authored entity: who wrote it and on what authority.
|
|
371
360
|
origin: ENTITY_ORIGIN_SCHEMA,
|
|
361
|
+
firmness: {
|
|
362
|
+
type: "string",
|
|
363
|
+
enum: ["firm", "default", "exploratory"],
|
|
364
|
+
description:
|
|
365
|
+
"Requirement and ADR only. How firmly the decision was made. firm: a human stated it as a rule; it needs a ref (origin.ref, or approval_ref on a requirement) to their words, and only a human may change it. default (assumed when absent): a delegated or agent choice; a later agent may supersede it with evidence. exploratory: a flagged guess, expected to be revisited.",
|
|
366
|
+
},
|
|
372
367
|
expects: {
|
|
373
368
|
type: "string",
|
|
374
369
|
enum: ["success", "rejection", "error"],
|
|
@@ -724,6 +719,12 @@ const entitySchema: Record<string, unknown> = {
|
|
|
724
719
|
},
|
|
725
720
|
},
|
|
726
721
|
},
|
|
722
|
+
{
|
|
723
|
+
if: {
|
|
724
|
+
properties: { type: { enum: ["req", "adr"] } },
|
|
725
|
+
},
|
|
726
|
+
else: { not: { required: ["firmness"] } },
|
|
727
|
+
},
|
|
727
728
|
{
|
|
728
729
|
if: {
|
|
729
730
|
properties: { type: { const: "scenario" } },
|
|
@@ -43,10 +43,12 @@ sequential `kb_upsert`, then run final check and coverage. Kibi authors tracked
|
|
|
43
43
|
files transactionally but never Git-stages or commits them. Do not read or edit
|
|
44
44
|
`.kb` directly.
|
|
45
45
|
|
|
46
|
-
Use `kb_delete` only for intentional cleanup
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
Use `kb_delete` only for intentional cleanup; `dryRun: true` previews any
|
|
47
|
+
delete without writing. Authored entity deletion returns `outcome:
|
|
48
|
+
plan_returned` with a hash-bound plan whose `nextActions[0].input` is the exact
|
|
49
|
+
`kb_apply_plan` input; requirements return `supersession_required` and evolve
|
|
50
|
+
through a new requirement linked with `supersedes`. Relationship deletes patch
|
|
51
|
+
shards while preserving unrelated records.
|
|
50
52
|
|
|
51
53
|
## Typed recovery and proof
|
|
52
54
|
|