kibi-cli 2.28.1 → 2.30.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/bin/kibi +12 -1
- package/dist/cli-command.d.ts +3 -0
- package/dist/cli-command.d.ts.map +1 -1
- package/dist/cli-command.js +11 -0
- package/dist/cli-operation-metadata.d.ts +8 -8
- package/dist/cli-operation-metadata.js +8 -8
- package/dist/cli-register-reporting.d.ts.map +1 -1
- package/dist/cli-register-reporting.js +12 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +5 -2
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +8 -1
- package/dist/commands/engine.d.ts.map +1 -1
- package/dist/commands/engine.js +26 -2
- package/dist/commands/signoff.d.ts +37 -0
- package/dist/commands/signoff.d.ts.map +1 -0
- package/dist/commands/signoff.js +152 -0
- package/dist/commands/sync.d.ts +10 -0
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +67 -3
- package/dist/engine.d.ts +11 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +120 -34
- package/dist/operations/modeling/predicate-applicability.d.ts +5 -1
- package/dist/operations/modeling/predicate-applicability.d.ts.map +1 -1
- package/dist/operations/modeling/predicate-applicability.js +35 -2
- package/dist/operations/modeling/suggest-predicates.d.ts.map +1 -1
- package/dist/operations/modeling/suggest-predicates.js +3 -1
- package/dist/operations/mutation/contradictions.d.ts +7 -5
- package/dist/operations/mutation/contradictions.d.ts.map +1 -1
- package/dist/operations/mutation/contradictions.js +8 -6
- package/dist/operations/mutation/source-authoring.d.ts +15 -0
- package/dist/operations/mutation/source-authoring.d.ts.map +1 -1
- package/dist/operations/mutation/source-authoring.js +19 -7
- package/dist/operations/mutation/types.d.ts +5 -0
- package/dist/operations/mutation/types.d.ts.map +1 -1
- package/dist/operations/mutation/upsert.d.ts +14 -0
- package/dist/operations/mutation/upsert.d.ts.map +1 -1
- package/dist/operations/mutation/upsert.js +42 -36
- package/dist/operations/mutation/validate-upsert.d.ts.map +1 -1
- package/dist/operations/mutation/validate-upsert.js +61 -37
- package/dist/prolog/error-terms.d.ts.map +1 -1
- package/dist/prolog/error-terms.js +15 -9
- package/dist/prolog/store-lock.d.ts +11 -0
- package/dist/prolog/store-lock.d.ts.map +1 -1
- package/dist/prolog/store-lock.js +48 -1
- package/dist/public/operations/catalog.d.ts +29 -8
- package/dist/public/operations/catalog.d.ts.map +1 -1
- package/dist/public/operations/contracts.d.ts.map +1 -1
- package/dist/public/operations/contracts.js +2 -0
- package/dist/public/operations/specs/bootstrap.d.ts +1 -1
- package/dist/public/operations/specs/bootstrap.js +1 -1
- package/dist/public/operations/specs/composite.d.ts +1 -1
- package/dist/public/operations/specs/composite.js +1 -1
- package/dist/public/operations/specs/discovery.d.ts +1 -1
- package/dist/public/operations/specs/discovery.js +1 -1
- package/dist/public/operations/specs/impact-review-preparation.d.ts +2 -1
- package/dist/public/operations/specs/impact-review-preparation.d.ts.map +1 -1
- package/dist/public/operations/specs/impact-review-preparation.js +2 -1
- 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 +23 -3
- package/dist/public/operations/specs/reporting.d.ts.map +1 -1
- package/dist/public/operations/specs/reporting.js +97 -21
- package/dist/public/skills/kibi-commit-review/SKILL.md +125 -0
- package/dist/public/skills/kibi-commit-review/resources/staged-gate.md +98 -0
- package/dist/public/skills/kibi-decisions/SKILL.md +130 -0
- package/dist/public/skills/kibi-decisions/resources/decision-records.md +142 -0
- package/dist/public/skills/kibi-decisions/resources/signoff-views.md +66 -0
- package/dist/public/skills/kibi-freshness/SKILL.md +2 -0
- package/dist/public/skills/kibi-traceability/SKILL.md +4 -0
- package/dist/public/skills/kibi-usage/SKILL.md +20 -15
- package/dist/signoff/collect.d.ts +39 -0
- package/dist/signoff/collect.d.ts.map +1 -0
- package/dist/signoff/collect.js +398 -0
- package/dist/signoff/html.d.ts +4 -0
- package/dist/signoff/html.d.ts.map +1 -0
- package/dist/signoff/html.js +210 -0
- package/dist/signoff/markdown.d.ts +20 -0
- package/dist/signoff/markdown.d.ts.map +1 -0
- package/dist/signoff/markdown.js +203 -0
- package/dist/signoff/model.d.ts +134 -0
- package/dist/signoff/model.d.ts.map +1 -0
- package/dist/signoff/model.js +138 -0
- package/dist/traceability/commit-amend.d.ts +7 -0
- package/dist/traceability/commit-amend.d.ts.map +1 -0
- package/dist/traceability/commit-amend.js +85 -0
- package/dist/traceability/staged-inventory-drift.d.ts +12 -2
- package/dist/traceability/staged-inventory-drift.d.ts.map +1 -1
- package/dist/traceability/staged-inventory-drift.js +22 -5
- package/package.json +2 -2
- package/src/public/operations/contracts.ts +2 -0
- package/src/public/operations/specs/bootstrap.ts +1 -1
- package/src/public/operations/specs/composite.ts +1 -1
- package/src/public/operations/specs/discovery.ts +1 -1
- package/src/public/operations/specs/impact-review-preparation.ts +3 -1
- package/src/public/operations/specs/planning.ts +1 -1
- package/src/public/operations/specs/reporting.ts +109 -21
- package/src/public/skills/kibi-commit-review/SKILL.md +125 -0
- package/src/public/skills/kibi-commit-review/resources/staged-gate.md +98 -0
- package/src/public/skills/kibi-decisions/SKILL.md +130 -0
- package/src/public/skills/kibi-decisions/resources/decision-records.md +142 -0
- package/src/public/skills/kibi-decisions/resources/signoff-views.md +66 -0
- package/src/public/skills/kibi-freshness/SKILL.md +2 -0
- package/src/public/skills/kibi-traceability/SKILL.md +4 -0
- package/src/public/skills/kibi-usage/SKILL.md +20 -15
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { SIGNOFF_CHANGE_LABELS, SIGNOFF_FENCES, approvalNumbers, approvalSentence, awaitsApproval, proofLabel, questionKind, questionOpen, statementSentenceDiff, } from "./model.js";
|
|
2
|
+
/** Rows shown in chat before the rest is left to the HTML view. */
|
|
3
|
+
export const SIGNOFF_MARKDOWN_LIMIT = 15;
|
|
4
|
+
const CHANGE_ORDER = [
|
|
5
|
+
"new",
|
|
6
|
+
"changed",
|
|
7
|
+
"affected",
|
|
8
|
+
"retired",
|
|
9
|
+
];
|
|
10
|
+
function quote(text) {
|
|
11
|
+
return `> ${text.replace(/\s+/g, " ").trim()}`;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A rewritten statement as lines under a list item: the new statement with
|
|
15
|
+
* added sentences in bold and removed ones struck through when sentences
|
|
16
|
+
* survive the rewrite, or the old and new statements otherwise.
|
|
17
|
+
*/
|
|
18
|
+
function rewriteLines(previous, statement, labels = ["Was", "Now"]) {
|
|
19
|
+
const diff = statementSentenceDiff(previous, statement);
|
|
20
|
+
if (diff === undefined) {
|
|
21
|
+
return [
|
|
22
|
+
`- ${labels[0]}:`,
|
|
23
|
+
` ${quote(previous)}`,
|
|
24
|
+
`- ${labels[1]}:`,
|
|
25
|
+
` ${quote(statement)}`,
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
const added = diff.filter((part) => part.change === "added").length;
|
|
29
|
+
const removed = diff.filter((part) => part.change === "removed").length;
|
|
30
|
+
const counts = [
|
|
31
|
+
added > 0 ? `${added} added in bold` : undefined,
|
|
32
|
+
removed > 0 ? `${removed} removed struck through` : undefined,
|
|
33
|
+
].filter(Boolean);
|
|
34
|
+
const text = diff
|
|
35
|
+
.map((part) => part.change === "added"
|
|
36
|
+
? `**${part.text}**`
|
|
37
|
+
: part.change === "removed"
|
|
38
|
+
? `~~${part.text}~~`
|
|
39
|
+
: part.text)
|
|
40
|
+
.join(" ");
|
|
41
|
+
return [
|
|
42
|
+
`- ${labels[1]}, sentences ${counts.join(", ")}:`,
|
|
43
|
+
` ${quote(text)}`,
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
function firmnessTag(firmness) {
|
|
47
|
+
return firmness === "firm"
|
|
48
|
+
? "firm"
|
|
49
|
+
: firmness === "exploratory"
|
|
50
|
+
? "exploratory"
|
|
51
|
+
: "default";
|
|
52
|
+
}
|
|
53
|
+
function originLine(requirement) {
|
|
54
|
+
const parts = [];
|
|
55
|
+
if (requirement.originKind)
|
|
56
|
+
parts.push(requirement.originKind);
|
|
57
|
+
if (requirement.originRef)
|
|
58
|
+
parts.push(`"${requirement.originRef}"`);
|
|
59
|
+
if (requirement.approvedBy)
|
|
60
|
+
parts.push(`approved by ${requirement.approvedBy}`);
|
|
61
|
+
if (awaitsApproval(requirement))
|
|
62
|
+
parts.push("**awaiting your approval**");
|
|
63
|
+
return parts.length > 0 ? `- Origin: ${parts.join(", ")}` : undefined;
|
|
64
|
+
}
|
|
65
|
+
function requirementBlock(requirement, number) {
|
|
66
|
+
const lines = [
|
|
67
|
+
`**${number}. ${requirement.id}** · ${firmnessTag(requirement.firmness)} · ${requirement.title}`,
|
|
68
|
+
];
|
|
69
|
+
if (requirement.previousStatement !== undefined) {
|
|
70
|
+
lines.push(...rewriteLines(requirement.previousStatement, requirement.statement));
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
lines.push(quote(requirement.statement));
|
|
74
|
+
}
|
|
75
|
+
const origin = originLine(requirement);
|
|
76
|
+
if (origin)
|
|
77
|
+
lines.push(origin);
|
|
78
|
+
for (const superseded of requirement.supersedes) {
|
|
79
|
+
lines.push(`- Replaces ${superseded.id} (${firmnessTag(superseded.firmness)}): ${superseded.statement}`);
|
|
80
|
+
}
|
|
81
|
+
if (requirement.symbols.length > 0) {
|
|
82
|
+
lines.push(`- Changed code: ${requirement.symbols.join(", ")}`);
|
|
83
|
+
}
|
|
84
|
+
if (requirement.change !== "retired") {
|
|
85
|
+
lines.push(requirement.scenarios.length > 0
|
|
86
|
+
? `- Scenarios: ${requirement.scenarios.map((scenario) => (scenario.title === scenario.id ? scenario.id : `${scenario.id} (${scenario.title})`)).join("; ")}`
|
|
87
|
+
: "- Scenarios: none yet");
|
|
88
|
+
lines.push(`- Proof: ${proofLabel(requirement.proof)}`);
|
|
89
|
+
for (const conflict of requirement.proof?.contradictions ?? []) {
|
|
90
|
+
lines.push(`- Contradiction: ${conflict}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return lines;
|
|
94
|
+
}
|
|
95
|
+
function headline(summary) {
|
|
96
|
+
const counts = CHANGE_ORDER.map((change) => [
|
|
97
|
+
change,
|
|
98
|
+
summary.requirements.filter((row) => row.change === change).length,
|
|
99
|
+
])
|
|
100
|
+
.filter(([, count]) => count > 0)
|
|
101
|
+
.map(([change, count]) => `${count} ${change}`);
|
|
102
|
+
return counts.length === 0
|
|
103
|
+
? "## Requirements signoff: no requirement changes"
|
|
104
|
+
: `## Requirements signoff: ${counts.join(", ")}`;
|
|
105
|
+
}
|
|
106
|
+
// implements REQ-kibi-signoff-views
|
|
107
|
+
/** The end-of-work summary as Markdown, without the fence. */
|
|
108
|
+
export function renderSummaryMarkdownBody(summary, limit = SIGNOFF_MARKDOWN_LIMIT) {
|
|
109
|
+
const lines = [headline(summary)];
|
|
110
|
+
const approvals = approvalNumbers(CHANGE_ORDER.flatMap((change) => summary.requirements.filter((row) => row.change === change)));
|
|
111
|
+
if (approvals.length > 0)
|
|
112
|
+
lines.push("", `**${approvalSentence(approvals)}**`);
|
|
113
|
+
let number = 0;
|
|
114
|
+
for (const change of CHANGE_ORDER) {
|
|
115
|
+
const rows = summary.requirements.filter((row) => row.change === change);
|
|
116
|
+
const shown = rows.slice(0, Math.max(0, limit - number));
|
|
117
|
+
if (shown.length === 0)
|
|
118
|
+
continue;
|
|
119
|
+
lines.push("", `### ${SIGNOFF_CHANGE_LABELS[change]}`);
|
|
120
|
+
for (const row of shown) {
|
|
121
|
+
number += 1;
|
|
122
|
+
lines.push("", ...requirementBlock(row, number));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const hidden = summary.requirements.length - limit;
|
|
126
|
+
if (hidden > 0) {
|
|
127
|
+
lines.push("", `…and ${hidden} more. Run \`kibi signoff --format html --open\` to see them all.`);
|
|
128
|
+
}
|
|
129
|
+
if (approvals.length > 0) {
|
|
130
|
+
lines.push("", 'Reply by number, for example "approve 1" or "1: reword …".');
|
|
131
|
+
}
|
|
132
|
+
for (const note of summary.notes)
|
|
133
|
+
lines.push("", `Note: ${note}`);
|
|
134
|
+
return lines.join("\n");
|
|
135
|
+
}
|
|
136
|
+
// implements REQ-kibi-signoff-views
|
|
137
|
+
/** The mid-work decision questions as Markdown, without the fence. */
|
|
138
|
+
export function renderQuestionsMarkdownBody(view) {
|
|
139
|
+
const open = view.questions.filter(questionOpen).length;
|
|
140
|
+
const answered = view.questions.length - open;
|
|
141
|
+
const lines = [
|
|
142
|
+
`## Decisions to confirm: ${open} open${answered > 0 ? `, ${answered} answered` : ""}`,
|
|
143
|
+
];
|
|
144
|
+
for (const question of view.questions) {
|
|
145
|
+
const title = question.title ?? question.current?.title ?? question.id;
|
|
146
|
+
const heading = `${question.number}. ${questionKind(question)}${title ? `: ${title}` : ""}`;
|
|
147
|
+
lines.push("");
|
|
148
|
+
if (!questionOpen(question)) {
|
|
149
|
+
lines.push(`~~**${heading}**~~ · answered: ${question.answer}`);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
lines.push(`**${heading}**${question.firmness ? ` · would be ${question.firmness}` : ""}`);
|
|
153
|
+
if (question.current) {
|
|
154
|
+
lines.push(...rewriteLines(question.current.statement, question.statement, [
|
|
155
|
+
`Recorded now (${firmnessTag(question.current.firmness)})`,
|
|
156
|
+
"Proposed",
|
|
157
|
+
]));
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
lines.push(quote(question.statement));
|
|
161
|
+
}
|
|
162
|
+
if (question.quote)
|
|
163
|
+
lines.push(`- Your words: "${question.quote}"`);
|
|
164
|
+
if (question.governs?.length) {
|
|
165
|
+
lines.push(`- Governs: ${question.governs.join(", ")}`);
|
|
166
|
+
}
|
|
167
|
+
for (const conflict of question.conflicts) {
|
|
168
|
+
lines.push(`- Conflicts with ${conflict.id} (${firmnessTag(conflict.firmness)}): ${conflict.statement}`);
|
|
169
|
+
}
|
|
170
|
+
if (question.question)
|
|
171
|
+
lines.push(`- Question: ${question.question}`);
|
|
172
|
+
}
|
|
173
|
+
if (open > 0) {
|
|
174
|
+
lines.push("", 'Reply by number, for example "1 yes, 2 exploratory, 3 reword: …".');
|
|
175
|
+
}
|
|
176
|
+
for (const note of view.notes)
|
|
177
|
+
lines.push("", `Note: ${note}`);
|
|
178
|
+
return lines.join("\n");
|
|
179
|
+
}
|
|
180
|
+
// implements REQ-kibi-signoff-views
|
|
181
|
+
/**
|
|
182
|
+
* A signoff view as plain Markdown for a chat reply or a messaging tool
|
|
183
|
+
* (headings, quotes and lists that any Markdown surface renders), with no
|
|
184
|
+
* fence for a host renderer to find.
|
|
185
|
+
*/
|
|
186
|
+
export function renderSignoffChatMarkdown(view) {
|
|
187
|
+
return view.kind === "summary"
|
|
188
|
+
? renderSummaryMarkdownBody(view)
|
|
189
|
+
: renderQuestionsMarkdownBody(view);
|
|
190
|
+
}
|
|
191
|
+
// implements REQ-kibi-signoff-views
|
|
192
|
+
/**
|
|
193
|
+
* A signoff view as the fenced block agents paste into chat. Host renderers
|
|
194
|
+
* (the Claude Code display hook, the OpenCode TUI) key on the fence's info
|
|
195
|
+
* string; hosts without one show it as a code block, so they get `chat`.
|
|
196
|
+
*/
|
|
197
|
+
export function renderSignoffMarkdown(view) {
|
|
198
|
+
const fence = SIGNOFF_FENCES[view.kind];
|
|
199
|
+
const body = renderSignoffChatMarkdown(view);
|
|
200
|
+
// A body line starting with ``` would close the fence early.
|
|
201
|
+
const safe = body.replace(/^```/gm, "'''");
|
|
202
|
+
return `\`\`\`${fence}\n${safe}\n\`\`\`\n`;
|
|
203
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Requirement signoff views. A person signs off requirements at two moments:
|
|
3
|
+
* mid-work, when the agent asks them to confirm rules before recording them
|
|
4
|
+
* (questions), and at the end of the work, before the commit (summary). Both
|
|
5
|
+
* views are one JSON document that every host renders: the CLI prints it as a
|
|
6
|
+
* fenced Markdown block or a self-contained HTML page, and editor plugins read
|
|
7
|
+
* the copies written to `.kb/briefs/signoff/` (latest.json, latest.md and
|
|
8
|
+
* latest.html; `.kb/briefs/` is git-ignored and never synced).
|
|
9
|
+
*/
|
|
10
|
+
export declare const SIGNOFF_VERSION = "kibi.signoff.v1";
|
|
11
|
+
/** The fence info strings host renderers key on. */
|
|
12
|
+
export declare const SIGNOFF_FENCES: {
|
|
13
|
+
readonly summary: "kibi-signoff";
|
|
14
|
+
readonly questions: "kibi-questions";
|
|
15
|
+
};
|
|
16
|
+
/** Where the latest signoff view is written, relative to the workspace. */
|
|
17
|
+
export declare const SIGNOFF_BRIEF_DIR = ".kb/briefs/signoff";
|
|
18
|
+
export declare const SIGNOFF_LATEST_JSON = ".kb/briefs/signoff/latest.json";
|
|
19
|
+
/** The latest view's Markdown body (no fence), for plain-text host views. */
|
|
20
|
+
export declare const SIGNOFF_LATEST_MARKDOWN = ".kb/briefs/signoff/latest.md";
|
|
21
|
+
/** The latest view as a self-contained page, for browsers and webviews. */
|
|
22
|
+
export declare const SIGNOFF_LATEST_HTML = ".kb/briefs/signoff/latest.html";
|
|
23
|
+
export type SignoffFirmness = "firm" | "default" | "exploratory";
|
|
24
|
+
/** How a requirement entered the change. */
|
|
25
|
+
export type SignoffChange = "new" | "changed" | "affected" | "retired";
|
|
26
|
+
export type SignoffStatementRef = Readonly<{
|
|
27
|
+
id: string;
|
|
28
|
+
title: string;
|
|
29
|
+
statement: string;
|
|
30
|
+
firmness?: SignoffFirmness;
|
|
31
|
+
}>;
|
|
32
|
+
export type SignoffProof = Readonly<{
|
|
33
|
+
/** Coverage proof status: proven, missing, unresolved or not_applicable. */
|
|
34
|
+
status: string;
|
|
35
|
+
/** The earliest unmet proof gate, or "proven". */
|
|
36
|
+
gate: string;
|
|
37
|
+
testCount: number;
|
|
38
|
+
contradictions: readonly string[];
|
|
39
|
+
}>;
|
|
40
|
+
export type SignoffRequirement = Readonly<{
|
|
41
|
+
id: string;
|
|
42
|
+
title: string;
|
|
43
|
+
change: SignoffChange;
|
|
44
|
+
status: string;
|
|
45
|
+
statement: string;
|
|
46
|
+
/** The statement at the base commit, when the change rewrote it. */
|
|
47
|
+
previousStatement?: string;
|
|
48
|
+
firmness?: SignoffFirmness;
|
|
49
|
+
originKind?: string;
|
|
50
|
+
originRef?: string;
|
|
51
|
+
approvedBy?: string;
|
|
52
|
+
supersedes: readonly SignoffStatementRef[];
|
|
53
|
+
scenarios: readonly Readonly<{
|
|
54
|
+
id: string;
|
|
55
|
+
title: string;
|
|
56
|
+
}>[];
|
|
57
|
+
/** Changed code symbols that implement it (affected requirements). */
|
|
58
|
+
symbols: readonly string[];
|
|
59
|
+
proof?: SignoffProof;
|
|
60
|
+
source: string;
|
|
61
|
+
}>;
|
|
62
|
+
export type SignoffSummary = Readonly<{
|
|
63
|
+
version: typeof SIGNOFF_VERSION;
|
|
64
|
+
kind: "summary";
|
|
65
|
+
scope: "staged" | "working";
|
|
66
|
+
generatedAt: string;
|
|
67
|
+
branch?: string;
|
|
68
|
+
requirements: readonly SignoffRequirement[];
|
|
69
|
+
/** Parts of the view Kibi could not compute, in plain words. */
|
|
70
|
+
notes: readonly string[];
|
|
71
|
+
}>;
|
|
72
|
+
/** One decision the agent asks the person to confirm. */
|
|
73
|
+
export type SignoffQuestionInput = Readonly<{
|
|
74
|
+
/** Existing or proposed requirement id. */
|
|
75
|
+
id?: string;
|
|
76
|
+
title?: string;
|
|
77
|
+
/** The rule as it would be recorded. */
|
|
78
|
+
statement: string;
|
|
79
|
+
/** The person's words that would go in origin.ref. */
|
|
80
|
+
quote?: string;
|
|
81
|
+
firmness?: SignoffFirmness;
|
|
82
|
+
/** Files or symbols the rule governs. */
|
|
83
|
+
governs?: readonly string[];
|
|
84
|
+
/** Recorded requirements the rule conflicts with. */
|
|
85
|
+
conflictsWith?: readonly string[];
|
|
86
|
+
/** What the agent asks, when it is more than "record this?". */
|
|
87
|
+
question?: string;
|
|
88
|
+
/** The person's answer, once given. */
|
|
89
|
+
answer?: string;
|
|
90
|
+
}>;
|
|
91
|
+
export type SignoffQuestion = SignoffQuestionInput & Readonly<{
|
|
92
|
+
number: number;
|
|
93
|
+
/** The recorded requirement this question would change. */
|
|
94
|
+
current?: SignoffStatementRef;
|
|
95
|
+
conflicts: readonly SignoffStatementRef[];
|
|
96
|
+
}>;
|
|
97
|
+
export type SignoffQuestions = Readonly<{
|
|
98
|
+
version: typeof SIGNOFF_VERSION;
|
|
99
|
+
kind: "questions";
|
|
100
|
+
generatedAt: string;
|
|
101
|
+
questions: readonly SignoffQuestion[];
|
|
102
|
+
notes: readonly string[];
|
|
103
|
+
}>;
|
|
104
|
+
export type SignoffView = SignoffSummary | SignoffQuestions;
|
|
105
|
+
export declare const SIGNOFF_CHANGE_LABELS: Readonly<Record<SignoffChange, string>>;
|
|
106
|
+
export declare const SIGNOFF_GATE_LABELS: Readonly<Record<string, string>>;
|
|
107
|
+
/** A short proof label for one requirement. */
|
|
108
|
+
export declare function proofLabel(proof: SignoffProof | undefined): string;
|
|
109
|
+
/** Whether a question still waits for the person. */
|
|
110
|
+
export declare function questionOpen(question: SignoffQuestionInput): boolean;
|
|
111
|
+
/** The kind of decision a question asks for. */
|
|
112
|
+
export declare function questionKind(question: SignoffQuestion): string;
|
|
113
|
+
/** One sentence of a rewritten statement and how the rewrite changed it. */
|
|
114
|
+
export type StatementSentence = Readonly<{
|
|
115
|
+
text: string;
|
|
116
|
+
change: "kept" | "added" | "removed";
|
|
117
|
+
}>;
|
|
118
|
+
/**
|
|
119
|
+
* A rewrite of a statement as its sentences in reading order, each kept,
|
|
120
|
+
* added or removed (a longest-common-subsequence diff), so the person reads
|
|
121
|
+
* what changed instead of comparing two paragraphs. Undefined when no
|
|
122
|
+
* sentence survives the rewrite, where before and after reads better.
|
|
123
|
+
*/
|
|
124
|
+
export declare function statementSentenceDiff(previous: string, statement: string): readonly StatementSentence[] | undefined;
|
|
125
|
+
/**
|
|
126
|
+
* Whether the change asks the person to approve a requirement: the agent
|
|
127
|
+
* wrote or rewrote it and nobody has approved it.
|
|
128
|
+
*/
|
|
129
|
+
export declare function awaitsApproval(requirement: SignoffRequirement): boolean;
|
|
130
|
+
/** The numbers (in view order) of the requirements awaiting approval. */
|
|
131
|
+
export declare function approvalNumbers(ordered: readonly SignoffRequirement[]): number[];
|
|
132
|
+
/** One sentence naming the requirements that wait for the person. */
|
|
133
|
+
export declare function approvalSentence(numbers: readonly number[]): string;
|
|
134
|
+
//# sourceMappingURL=model.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../src/signoff/model.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Requirement signoff views. A person signs off requirements at two moments:
|
|
3
|
+
* mid-work, when the agent asks them to confirm rules before recording them
|
|
4
|
+
* (questions), and at the end of the work, before the commit (summary). Both
|
|
5
|
+
* views are one JSON document that every host renders: the CLI prints it as a
|
|
6
|
+
* fenced Markdown block or a self-contained HTML page, and editor plugins read
|
|
7
|
+
* the copies written to `.kb/briefs/signoff/` (latest.json, latest.md and
|
|
8
|
+
* latest.html; `.kb/briefs/` is git-ignored and never synced).
|
|
9
|
+
*/
|
|
10
|
+
export const SIGNOFF_VERSION = "kibi.signoff.v1";
|
|
11
|
+
/** The fence info strings host renderers key on. */
|
|
12
|
+
export const SIGNOFF_FENCES = {
|
|
13
|
+
summary: "kibi-signoff",
|
|
14
|
+
questions: "kibi-questions",
|
|
15
|
+
};
|
|
16
|
+
/** Where the latest signoff view is written, relative to the workspace. */
|
|
17
|
+
export const SIGNOFF_BRIEF_DIR = ".kb/briefs/signoff";
|
|
18
|
+
export const SIGNOFF_LATEST_JSON = `${SIGNOFF_BRIEF_DIR}/latest.json`;
|
|
19
|
+
/** The latest view's Markdown body (no fence), for plain-text host views. */
|
|
20
|
+
export const SIGNOFF_LATEST_MARKDOWN = `${SIGNOFF_BRIEF_DIR}/latest.md`;
|
|
21
|
+
/** The latest view as a self-contained page, for browsers and webviews. */
|
|
22
|
+
export const SIGNOFF_LATEST_HTML = `${SIGNOFF_BRIEF_DIR}/latest.html`;
|
|
23
|
+
export const SIGNOFF_CHANGE_LABELS = {
|
|
24
|
+
new: "New",
|
|
25
|
+
changed: "Changed",
|
|
26
|
+
affected: "Affected by the code change (statement unchanged)",
|
|
27
|
+
retired: "Superseded or retired",
|
|
28
|
+
};
|
|
29
|
+
export const SIGNOFF_GATE_LABELS = {
|
|
30
|
+
semantic: "semantic model incomplete",
|
|
31
|
+
scenario: "no scenario yet",
|
|
32
|
+
implementation: "no owning code yet",
|
|
33
|
+
e2e: "no end-to-end test yet",
|
|
34
|
+
evidence: "end-to-end evidence not current",
|
|
35
|
+
proven: "proven",
|
|
36
|
+
};
|
|
37
|
+
/** A short proof label for one requirement. */
|
|
38
|
+
export function proofLabel(proof) {
|
|
39
|
+
if (proof === undefined)
|
|
40
|
+
return "not checked";
|
|
41
|
+
if (proof.status === "not_applicable")
|
|
42
|
+
return "not applicable";
|
|
43
|
+
const gate = SIGNOFF_GATE_LABELS[proof.gate] ?? proof.gate;
|
|
44
|
+
const tests = `${proof.testCount} ${proof.testCount === 1 ? "test" : "tests"}`;
|
|
45
|
+
return proof.gate === "proven" ? `proven, ${tests}` : `${gate}, ${tests}`;
|
|
46
|
+
}
|
|
47
|
+
/** Whether a question still waits for the person. */
|
|
48
|
+
export function questionOpen(question) {
|
|
49
|
+
return (question.answer ?? "").trim() === "";
|
|
50
|
+
}
|
|
51
|
+
/** The kind of decision a question asks for. */
|
|
52
|
+
export function questionKind(question) {
|
|
53
|
+
if (question.current)
|
|
54
|
+
return `Change ${question.current.id}`;
|
|
55
|
+
if (question.conflicts.length > 0)
|
|
56
|
+
return "Conflict with a recorded rule";
|
|
57
|
+
return "New rule";
|
|
58
|
+
}
|
|
59
|
+
function sentences(statement) {
|
|
60
|
+
return statement
|
|
61
|
+
.replace(/\s+/g, " ")
|
|
62
|
+
.trim()
|
|
63
|
+
.split(/(?<=[.!?])\s+(?=[A-Z0-9"'`(])/)
|
|
64
|
+
.filter((sentence) => sentence.length > 0);
|
|
65
|
+
}
|
|
66
|
+
// implements REQ-kibi-signoff-views
|
|
67
|
+
/**
|
|
68
|
+
* A rewrite of a statement as its sentences in reading order, each kept,
|
|
69
|
+
* added or removed (a longest-common-subsequence diff), so the person reads
|
|
70
|
+
* what changed instead of comparing two paragraphs. Undefined when no
|
|
71
|
+
* sentence survives the rewrite, where before and after reads better.
|
|
72
|
+
*/
|
|
73
|
+
export function statementSentenceDiff(previous, statement) {
|
|
74
|
+
const before = sentences(previous);
|
|
75
|
+
const after = sentences(statement);
|
|
76
|
+
const table = before.map(() => new Array(after.length + 1).fill(0));
|
|
77
|
+
table.push(new Array(after.length + 1).fill(0));
|
|
78
|
+
for (let i = before.length - 1; i >= 0; i -= 1) {
|
|
79
|
+
for (let j = after.length - 1; j >= 0; j -= 1) {
|
|
80
|
+
const row = table[i];
|
|
81
|
+
const next = table[i + 1];
|
|
82
|
+
row[j] =
|
|
83
|
+
before[i] === after[j]
|
|
84
|
+
? next[j + 1] + 1
|
|
85
|
+
: Math.max(next[j], row[j + 1]);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if ((table[0]?.[0] ?? 0) === 0)
|
|
89
|
+
return undefined;
|
|
90
|
+
const result = [];
|
|
91
|
+
let i = 0;
|
|
92
|
+
let j = 0;
|
|
93
|
+
while (i < before.length || j < after.length) {
|
|
94
|
+
if (i < before.length && j < after.length && before[i] === after[j]) {
|
|
95
|
+
result.push({ text: after[j], change: "kept" });
|
|
96
|
+
i += 1;
|
|
97
|
+
j += 1;
|
|
98
|
+
}
|
|
99
|
+
else if (j < after.length &&
|
|
100
|
+
(i >= before.length ||
|
|
101
|
+
table[i]?.[j + 1] >= table[i + 1]?.[j])) {
|
|
102
|
+
result.push({ text: after[j], change: "added" });
|
|
103
|
+
j += 1;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
result.push({ text: before[i], change: "removed" });
|
|
107
|
+
i += 1;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
// implements REQ-kibi-signoff-views
|
|
113
|
+
/**
|
|
114
|
+
* Whether the change asks the person to approve a requirement: the agent
|
|
115
|
+
* wrote or rewrote it and nobody has approved it.
|
|
116
|
+
*/
|
|
117
|
+
export function awaitsApproval(requirement) {
|
|
118
|
+
return ((requirement.change === "new" || requirement.change === "changed") &&
|
|
119
|
+
requirement.originKind === "agent" &&
|
|
120
|
+
!requirement.approvedBy);
|
|
121
|
+
}
|
|
122
|
+
// implements REQ-kibi-signoff-views
|
|
123
|
+
/** The numbers (in view order) of the requirements awaiting approval. */
|
|
124
|
+
export function approvalNumbers(ordered) {
|
|
125
|
+
return ordered.flatMap((requirement, index) => awaitsApproval(requirement) ? [index + 1] : []);
|
|
126
|
+
}
|
|
127
|
+
// implements REQ-kibi-signoff-views
|
|
128
|
+
/** One sentence naming the requirements that wait for the person. */
|
|
129
|
+
export function approvalSentence(numbers) {
|
|
130
|
+
if (numbers.length === 0)
|
|
131
|
+
return "";
|
|
132
|
+
const list = numbers.length === 1
|
|
133
|
+
? `${numbers[0]}`
|
|
134
|
+
: `${numbers.slice(0, -1).join(", ")} and ${numbers.at(-1)}`;
|
|
135
|
+
return numbers.length === 1
|
|
136
|
+
? `Requirement ${list} was written by the agent and waits for your approval.`
|
|
137
|
+
: `Requirements ${list} were written by the agent and wait for your approval.`;
|
|
138
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the commit being made replaces a root commit (`git commit --amend`
|
|
3
|
+
* on a branch with one commit). Such a commit has no parent either, so the
|
|
4
|
+
* staged gate treats it as a first commit.
|
|
5
|
+
*/
|
|
6
|
+
export declare function amendsRootCommit(workspaceRoot: string, headCommit: string | null): boolean;
|
|
7
|
+
//# sourceMappingURL=commit-amend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commit-amend.d.ts","sourceRoot":"","sources":["../../src/traceability/commit-amend.ts"],"names":[],"mappings":"AAsEA;;;;GAIG;AAEH,wBAAgB,gBAAgB,CAC9B,aAAa,EAAE,MAAM,EACrB,UAAU,EAAE,MAAM,GAAG,IAAI,GACxB,OAAO,CAmBT"}
|
|
@@ -0,0 +1,85 @@
|
|
|
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 { basename } from "node:path";
|
|
21
|
+
/** How far up the process tree to look for the `git commit` running a hook. */
|
|
22
|
+
const MAX_ANCESTORS = 8;
|
|
23
|
+
function processArguments(pid) {
|
|
24
|
+
try {
|
|
25
|
+
if (process.platform === "linux") {
|
|
26
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
27
|
+
const parent = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
|
|
28
|
+
const argv = readFileSync(`/proc/${pid}/cmdline`, "utf8")
|
|
29
|
+
.split("\0")
|
|
30
|
+
.filter(Boolean);
|
|
31
|
+
return { parent, argv };
|
|
32
|
+
}
|
|
33
|
+
if (process.platform === "win32")
|
|
34
|
+
return null;
|
|
35
|
+
const [parent, ...argv] = execFileSync("ps", ["-o", "ppid=,args=", "-p", String(pid)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 2000 })
|
|
36
|
+
.trim()
|
|
37
|
+
.split(/\s+/);
|
|
38
|
+
return { parent: Number(parent), argv };
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Whether this process runs under `git commit --amend`. Git does not tell a
|
|
46
|
+
* pre-commit hook that it is amending, so the hook's ancestors are read.
|
|
47
|
+
*/
|
|
48
|
+
function runsUnderCommitAmend() {
|
|
49
|
+
let pid = process.ppid;
|
|
50
|
+
for (let depth = 0; depth < MAX_ANCESTORS && pid > 1; depth += 1) {
|
|
51
|
+
const info = processArguments(pid);
|
|
52
|
+
if (info === null)
|
|
53
|
+
return false;
|
|
54
|
+
const [program, ...rest] = info.argv;
|
|
55
|
+
if (program !== undefined && /^git(\.exe)?$/.test(basename(program)))
|
|
56
|
+
return rest.includes("commit") && rest.includes("--amend");
|
|
57
|
+
pid = info.parent;
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Whether the commit being made replaces a root commit (`git commit --amend`
|
|
63
|
+
* on a branch with one commit). Such a commit has no parent either, so the
|
|
64
|
+
* staged gate treats it as a first commit.
|
|
65
|
+
*/
|
|
66
|
+
// implements REQ-kibi-proposition-complete-ingestion
|
|
67
|
+
export function amendsRootCommit(workspaceRoot, headCommit) {
|
|
68
|
+
if (headCommit === null)
|
|
69
|
+
return false;
|
|
70
|
+
try {
|
|
71
|
+
const parents = execFileSync("git", ["rev-list", "--parents", "-n", "1", headCommit], {
|
|
72
|
+
cwd: workspaceRoot,
|
|
73
|
+
encoding: "utf8",
|
|
74
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
75
|
+
})
|
|
76
|
+
.trim()
|
|
77
|
+
.split(/\s+/);
|
|
78
|
+
if (parents.length > 1)
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
return runsUnderCommitAmend();
|
|
85
|
+
}
|
|
@@ -13,12 +13,22 @@ export type StagedInventoryDriftViolation = Readonly<{
|
|
|
13
13
|
* or in the base tree) whose inventory no longer matches their prose. The
|
|
14
14
|
* next `kibi sync` refuses them under the proposition-complete boundary;
|
|
15
15
|
* the gate applies the same test, so drift is caught at the commit that
|
|
16
|
-
* introduces it instead of after a later merge.
|
|
17
|
-
*
|
|
16
|
+
* introduces it instead of after a later merge. A requirement the base tree
|
|
17
|
+
* does not have yet is judged too, as sync judges a source it has never
|
|
18
|
+
* imported, unless the commit has no parent (a first commit, which sync
|
|
19
|
+
* imports as its semantic baseline). So is one whose statement (the prose
|
|
20
|
+
* its inventory covers) differs from the base tree's, as sync judges a
|
|
21
|
+
* source whose semantic hash changed. Other requirements without a contract,
|
|
22
|
+
* such as a Context-only edit, are left to sync's baseline.
|
|
18
23
|
*/
|
|
19
24
|
export declare function stagedInventoryDriftViolations(input: {
|
|
20
25
|
stagedPaths: ReadonlySet<string>;
|
|
21
26
|
snapshotEntities: readonly ExtractionResult[];
|
|
22
27
|
readBase: (path: string) => string | null;
|
|
28
|
+
/**
|
|
29
|
+
* False for a first commit, or an amend of it: sync grandfathers its sources
|
|
30
|
+
* as a baseline, so only declared contracts are judged.
|
|
31
|
+
*/
|
|
32
|
+
judgeNewRequirements?: boolean;
|
|
23
33
|
}): StagedInventoryDriftViolation[];
|
|
24
34
|
//# sourceMappingURL=staged-inventory-drift.d.ts.map
|
|
@@ -1 +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;
|
|
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;AA0CH;;;;;;;;;;;;GAYG;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;IAC1C;;;OAGG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC,GAAG,6BAA6B,EAAE,CAsClC"}
|
|
@@ -34,6 +34,11 @@ function payloadOf(result) {
|
|
|
34
34
|
relationships: result.relationships,
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
+
/** The hash of the prose a requirement's inventory must cover. */
|
|
38
|
+
function semanticSourceHashOf(result) {
|
|
39
|
+
const payload = payloadOf(result);
|
|
40
|
+
return validateSemanticInventoryBoundary(payload, result.relationships, analyzeSemanticAdvisorInput({ payload }).receipt).sourceHash;
|
|
41
|
+
}
|
|
37
42
|
function baseResult(path, readBase) {
|
|
38
43
|
const content = readBase(path);
|
|
39
44
|
if (content === null)
|
|
@@ -50,8 +55,13 @@ function baseResult(path, readBase) {
|
|
|
50
55
|
* or in the base tree) whose inventory no longer matches their prose. The
|
|
51
56
|
* next `kibi sync` refuses them under the proposition-complete boundary;
|
|
52
57
|
* the gate applies the same test, so drift is caught at the commit that
|
|
53
|
-
* introduces it instead of after a later merge.
|
|
54
|
-
*
|
|
58
|
+
* introduces it instead of after a later merge. A requirement the base tree
|
|
59
|
+
* does not have yet is judged too, as sync judges a source it has never
|
|
60
|
+
* imported, unless the commit has no parent (a first commit, which sync
|
|
61
|
+
* imports as its semantic baseline). So is one whose statement (the prose
|
|
62
|
+
* its inventory covers) differs from the base tree's, as sync judges a
|
|
63
|
+
* source whose semantic hash changed. Other requirements without a contract,
|
|
64
|
+
* such as a Context-only edit, are left to sync's baseline.
|
|
55
65
|
*/
|
|
56
66
|
// implements REQ-kibi-proposition-complete-ingestion
|
|
57
67
|
export function stagedInventoryDriftViolations(input) {
|
|
@@ -68,16 +78,23 @@ export function stagedInventoryDriftViolations(input) {
|
|
|
68
78
|
if (boundary.errors.length === 0)
|
|
69
79
|
continue;
|
|
70
80
|
const base = baseResult(source, input.readBase);
|
|
71
|
-
const
|
|
81
|
+
const contract = advertisesContract(result.entity) ||
|
|
72
82
|
(base !== null && advertisesContract(base.entity));
|
|
73
|
-
|
|
83
|
+
const judgeUncontracted = input.judgeNewRequirements ?? true;
|
|
84
|
+
const isNew = input.readBase(source) === null && judgeUncontracted;
|
|
85
|
+
const restated = judgeUncontracted &&
|
|
86
|
+
base !== null &&
|
|
87
|
+
semanticSourceHashOf(base) !== boundary.sourceHash;
|
|
88
|
+
if (!contract && !isNew && !restated)
|
|
74
89
|
continue;
|
|
75
90
|
violations.push({
|
|
76
91
|
rule: STAGED_INVENTORY_DRIFT_RULE,
|
|
77
92
|
entityId: result.entity.id,
|
|
78
93
|
source,
|
|
79
94
|
errors: boundary.errors,
|
|
80
|
-
message:
|
|
95
|
+
message: contract
|
|
96
|
+
? `${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.`
|
|
97
|
+
: `${result.entity.id} (${source}): ${isNew ? "a new requirement" : "a requirement whose statement changes"} needs a semantic inventory that matches its prose, so the next kibi sync rejects it: ${boundary.errors.join("; ")}. Run kb_model mode analyze (CLI: semantic-advisor) with its id and complete semantic_text, then write the returned inventory contract into it (kb_upsert with semantic_inventory, semantic_inventory_version, semantic_source_field, semantic_source_hash and logic_claims).`,
|
|
81
98
|
});
|
|
82
99
|
}
|
|
83
100
|
return violations.sort((left, right) => left.entityId.localeCompare(right.entityId));
|