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.
Files changed (106) hide show
  1. package/bin/kibi +12 -1
  2. package/dist/cli-command.d.ts +3 -0
  3. package/dist/cli-command.d.ts.map +1 -1
  4. package/dist/cli-command.js +11 -0
  5. package/dist/cli-operation-metadata.d.ts +8 -8
  6. package/dist/cli-operation-metadata.js +8 -8
  7. package/dist/cli-register-reporting.d.ts.map +1 -1
  8. package/dist/cli-register-reporting.js +12 -0
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cli.js +5 -2
  11. package/dist/commands/check.d.ts.map +1 -1
  12. package/dist/commands/check.js +8 -1
  13. package/dist/commands/engine.d.ts.map +1 -1
  14. package/dist/commands/engine.js +26 -2
  15. package/dist/commands/signoff.d.ts +37 -0
  16. package/dist/commands/signoff.d.ts.map +1 -0
  17. package/dist/commands/signoff.js +152 -0
  18. package/dist/commands/sync.d.ts +10 -0
  19. package/dist/commands/sync.d.ts.map +1 -1
  20. package/dist/commands/sync.js +67 -3
  21. package/dist/engine.d.ts +11 -1
  22. package/dist/engine.d.ts.map +1 -1
  23. package/dist/engine.js +120 -34
  24. package/dist/operations/modeling/predicate-applicability.d.ts +5 -1
  25. package/dist/operations/modeling/predicate-applicability.d.ts.map +1 -1
  26. package/dist/operations/modeling/predicate-applicability.js +35 -2
  27. package/dist/operations/modeling/suggest-predicates.d.ts.map +1 -1
  28. package/dist/operations/modeling/suggest-predicates.js +3 -1
  29. package/dist/operations/mutation/contradictions.d.ts +7 -5
  30. package/dist/operations/mutation/contradictions.d.ts.map +1 -1
  31. package/dist/operations/mutation/contradictions.js +8 -6
  32. package/dist/operations/mutation/source-authoring.d.ts +15 -0
  33. package/dist/operations/mutation/source-authoring.d.ts.map +1 -1
  34. package/dist/operations/mutation/source-authoring.js +19 -7
  35. package/dist/operations/mutation/types.d.ts +5 -0
  36. package/dist/operations/mutation/types.d.ts.map +1 -1
  37. package/dist/operations/mutation/upsert.d.ts +14 -0
  38. package/dist/operations/mutation/upsert.d.ts.map +1 -1
  39. package/dist/operations/mutation/upsert.js +42 -36
  40. package/dist/operations/mutation/validate-upsert.d.ts.map +1 -1
  41. package/dist/operations/mutation/validate-upsert.js +61 -37
  42. package/dist/prolog/error-terms.d.ts.map +1 -1
  43. package/dist/prolog/error-terms.js +15 -9
  44. package/dist/prolog/store-lock.d.ts +11 -0
  45. package/dist/prolog/store-lock.d.ts.map +1 -1
  46. package/dist/prolog/store-lock.js +48 -1
  47. package/dist/public/operations/catalog.d.ts +29 -8
  48. package/dist/public/operations/catalog.d.ts.map +1 -1
  49. package/dist/public/operations/contracts.d.ts.map +1 -1
  50. package/dist/public/operations/contracts.js +2 -0
  51. package/dist/public/operations/specs/bootstrap.d.ts +1 -1
  52. package/dist/public/operations/specs/bootstrap.js +1 -1
  53. package/dist/public/operations/specs/composite.d.ts +1 -1
  54. package/dist/public/operations/specs/composite.js +1 -1
  55. package/dist/public/operations/specs/discovery.d.ts +1 -1
  56. package/dist/public/operations/specs/discovery.js +1 -1
  57. package/dist/public/operations/specs/impact-review-preparation.d.ts +2 -1
  58. package/dist/public/operations/specs/impact-review-preparation.d.ts.map +1 -1
  59. package/dist/public/operations/specs/impact-review-preparation.js +2 -1
  60. package/dist/public/operations/specs/planning.d.ts +1 -1
  61. package/dist/public/operations/specs/planning.js +1 -1
  62. package/dist/public/operations/specs/reporting.d.ts +23 -3
  63. package/dist/public/operations/specs/reporting.d.ts.map +1 -1
  64. package/dist/public/operations/specs/reporting.js +97 -21
  65. package/dist/public/skills/kibi-commit-review/SKILL.md +125 -0
  66. package/dist/public/skills/kibi-commit-review/resources/staged-gate.md +98 -0
  67. package/dist/public/skills/kibi-decisions/SKILL.md +130 -0
  68. package/dist/public/skills/kibi-decisions/resources/decision-records.md +142 -0
  69. package/dist/public/skills/kibi-decisions/resources/signoff-views.md +66 -0
  70. package/dist/public/skills/kibi-freshness/SKILL.md +2 -0
  71. package/dist/public/skills/kibi-traceability/SKILL.md +4 -0
  72. package/dist/public/skills/kibi-usage/SKILL.md +20 -15
  73. package/dist/signoff/collect.d.ts +39 -0
  74. package/dist/signoff/collect.d.ts.map +1 -0
  75. package/dist/signoff/collect.js +398 -0
  76. package/dist/signoff/html.d.ts +4 -0
  77. package/dist/signoff/html.d.ts.map +1 -0
  78. package/dist/signoff/html.js +210 -0
  79. package/dist/signoff/markdown.d.ts +20 -0
  80. package/dist/signoff/markdown.d.ts.map +1 -0
  81. package/dist/signoff/markdown.js +203 -0
  82. package/dist/signoff/model.d.ts +134 -0
  83. package/dist/signoff/model.d.ts.map +1 -0
  84. package/dist/signoff/model.js +138 -0
  85. package/dist/traceability/commit-amend.d.ts +7 -0
  86. package/dist/traceability/commit-amend.d.ts.map +1 -0
  87. package/dist/traceability/commit-amend.js +85 -0
  88. package/dist/traceability/staged-inventory-drift.d.ts +12 -2
  89. package/dist/traceability/staged-inventory-drift.d.ts.map +1 -1
  90. package/dist/traceability/staged-inventory-drift.js +22 -5
  91. package/package.json +2 -2
  92. package/src/public/operations/contracts.ts +2 -0
  93. package/src/public/operations/specs/bootstrap.ts +1 -1
  94. package/src/public/operations/specs/composite.ts +1 -1
  95. package/src/public/operations/specs/discovery.ts +1 -1
  96. package/src/public/operations/specs/impact-review-preparation.ts +3 -1
  97. package/src/public/operations/specs/planning.ts +1 -1
  98. package/src/public/operations/specs/reporting.ts +109 -21
  99. package/src/public/skills/kibi-commit-review/SKILL.md +125 -0
  100. package/src/public/skills/kibi-commit-review/resources/staged-gate.md +98 -0
  101. package/src/public/skills/kibi-decisions/SKILL.md +130 -0
  102. package/src/public/skills/kibi-decisions/resources/decision-records.md +142 -0
  103. package/src/public/skills/kibi-decisions/resources/signoff-views.md +66 -0
  104. package/src/public/skills/kibi-freshness/SKILL.md +2 -0
  105. package/src/public/skills/kibi-traceability/SKILL.md +4 -0
  106. 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. Requirements without a
17
- * contract are left to sync, which decides from its own baseline.
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;AAgCH;;;;;;;GAOG;AAEH,wBAAgB,8BAA8B,CAAC,KAAK,EAAE;IACpD,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACjC,gBAAgB,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC9C,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;CAC3C,GAAG,6BAA6B,EAAE,CA8BlC"}
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. Requirements without a
54
- * contract are left to sync, which decides from its own baseline.
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 enforce = advertisesContract(result.entity) ||
81
+ const contract = advertisesContract(result.entity) ||
72
82
  (base !== null && advertisesContract(base.entity));
73
- if (!enforce)
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: `${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.`,
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));