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,398 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { effectiveFirmness } from "../decision-firmness.js";
5
+ import { extractFromMarkdownString, } from "../extractors/markdown.js";
6
+ import { readAllShards } from "../relationships/shards.js";
7
+ import { SIGNOFF_VERSION, } from "./model.js";
8
+ const REQUIREMENTS_DIR = ".kb/requirements";
9
+ const SCENARIOS_DIR = ".kb/scenarios";
10
+ const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
11
+ const RETIRED_STATUS = /supersed|deprecat|reject|obsolete|retired|archived|removed|closed/i;
12
+ const REQUIREMENT_ID = /^REQ-[A-Za-z0-9._-]+$/;
13
+ function gitRunner(workspaceRoot) {
14
+ return (args) => execFileSync("git", [...args], {
15
+ cwd: workspaceRoot,
16
+ encoding: "utf8",
17
+ stdio: ["ignore", "pipe", "pipe"],
18
+ maxBuffer: 64 * 1024 * 1024,
19
+ });
20
+ }
21
+ function tryGit(git, args) {
22
+ try {
23
+ return git(args);
24
+ }
25
+ catch {
26
+ return undefined;
27
+ }
28
+ }
29
+ function parseEntity(content, filePath) {
30
+ if (content === undefined)
31
+ return undefined;
32
+ try {
33
+ return extractFromMarkdownString(content, filePath).entity;
34
+ }
35
+ catch {
36
+ return undefined;
37
+ }
38
+ }
39
+ function flat(text) {
40
+ return text.replace(/\s+/g, " ").trim();
41
+ }
42
+ /** The requirement's statement: its semantic text, else its title. */
43
+ export function requirementStatement(entity) {
44
+ return flat(entity.semantic_text ?? entity.title ?? "");
45
+ }
46
+ function firmnessOf(entity) {
47
+ return effectiveFirmness(entity.type, entity.firmness, entity.origin);
48
+ }
49
+ function statementRef(entity) {
50
+ const firmness = firmnessOf(entity);
51
+ return {
52
+ id: entity.id,
53
+ title: flat(entity.title ?? entity.id),
54
+ statement: requirementStatement(entity),
55
+ ...(firmness ? { firmness } : {}),
56
+ };
57
+ }
58
+ /** Requirement documents the change adds, modifies or deletes. */
59
+ function changedRequirementFiles(git, scope, base) {
60
+ const args = scope === "staged"
61
+ ? ["diff", "--cached", "--name-status", "--no-renames", base]
62
+ : ["diff", "--name-status", "--no-renames", base];
63
+ const output = git([...args, "--", REQUIREMENTS_DIR]);
64
+ const files = [];
65
+ for (const line of output.split("\n")) {
66
+ const [status, file] = line.split("\t");
67
+ if (!file?.endsWith(".md"))
68
+ continue;
69
+ if (status === "A" || status === "M" || status === "D") {
70
+ files.push({ status, file });
71
+ }
72
+ }
73
+ if (scope === "working") {
74
+ const untracked = git([
75
+ "ls-files",
76
+ "--others",
77
+ "--exclude-standard",
78
+ "--",
79
+ REQUIREMENTS_DIR,
80
+ ]);
81
+ for (const file of untracked.split("\n")) {
82
+ if (file.endsWith(".md"))
83
+ files.push({ status: "A", file });
84
+ }
85
+ }
86
+ return files.sort((left, right) => left.file.localeCompare(right.file));
87
+ }
88
+ function currentContent(workspaceRoot, git, scope, file) {
89
+ if (scope === "staged")
90
+ return tryGit(git, ["show", `:${file}`]);
91
+ try {
92
+ return readFileSync(path.join(workspaceRoot, file), "utf8");
93
+ }
94
+ catch {
95
+ return undefined;
96
+ }
97
+ }
98
+ /** Requirement entities in the working tree, read lazily by id. */
99
+ function workspaceRequirements(workspaceRoot) {
100
+ const cache = new Map();
101
+ return (id) => {
102
+ if (!REQUIREMENT_ID.test(id))
103
+ return undefined;
104
+ if (!cache.has(id)) {
105
+ const file = path.join(workspaceRoot, REQUIREMENTS_DIR, `${id}.md`);
106
+ cache.set(id, existsSync(file)
107
+ ? parseEntity(readFileSync(file, "utf8"), file)
108
+ : undefined);
109
+ }
110
+ return cache.get(id);
111
+ };
112
+ }
113
+ function scenarioTitle(workspaceRoot, id) {
114
+ const file = path.join(workspaceRoot, SCENARIOS_DIR, `${id}.md`);
115
+ if (!/^SCEN-[A-Za-z0-9._-]+$/.test(id) || !existsSync(file))
116
+ return id;
117
+ return flat(parseEntity(readFileSync(file, "utf8"), file)?.title ?? id);
118
+ }
119
+ function readRelationships(workspaceRoot) {
120
+ try {
121
+ return readAllShards(path.join(workspaceRoot, ".kb"));
122
+ }
123
+ catch {
124
+ return [];
125
+ }
126
+ }
127
+ function headExists(git) {
128
+ return (tryGit(git, ["rev-parse", "--verify", "--quiet", "HEAD"]) !== undefined);
129
+ }
130
+ // implements REQ-kibi-signoff-views
131
+ /**
132
+ * The end-of-work signoff summary: the requirements the change adds, rewrites,
133
+ * retires, or leaves unchanged while changing code that implements them, each
134
+ * with its statement, firmness, origin, scenarios and proof state.
135
+ */
136
+ export async function collectSignoffSummary(workspaceRoot, scope, deps = {}) {
137
+ const git = deps.git ?? gitRunner(workspaceRoot);
138
+ const notes = [];
139
+ const base = headExists(git) ? "HEAD" : EMPTY_TREE;
140
+ const readRequirement = workspaceRequirements(workspaceRoot);
141
+ const relationships = readRelationships(workspaceRoot);
142
+ const outgoing = (id, type) => relationships
143
+ .filter((relationship) => relationship.from === id)
144
+ .filter((relationship) => relationship.type === type)
145
+ .map((relationship) => relationship.to);
146
+ const drafts = new Map();
147
+ const draftFor = (entity, change, source, previous) => {
148
+ const statement = requirementStatement(entity);
149
+ const previousStatement = previous
150
+ ? requirementStatement(previous)
151
+ : undefined;
152
+ const firmness = firmnessOf(entity);
153
+ const origin = entity.origin;
154
+ return {
155
+ id: entity.id,
156
+ title: flat(entity.title ?? entity.id),
157
+ change,
158
+ status: entity.status ?? "",
159
+ statement,
160
+ ...(previousStatement !== undefined && previousStatement !== statement
161
+ ? { previousStatement }
162
+ : {}),
163
+ ...(firmness ? { firmness } : {}),
164
+ ...(origin?.kind ? { originKind: origin.kind } : {}),
165
+ ...(origin?.ref ? { originRef: flat(origin.ref) } : {}),
166
+ ...(origin?.approved_by ? { approvedBy: origin.approved_by } : {}),
167
+ supersedes: outgoing(entity.id, "supersedes")
168
+ .map((id) => readRequirement(id))
169
+ .filter((ref) => ref !== undefined)
170
+ .map(statementRef),
171
+ scenarios: outgoing(entity.id, "specified_by")
172
+ .filter((id) => id.startsWith("SCEN-"))
173
+ .map((id) => ({ id, title: scenarioTitle(workspaceRoot, id) })),
174
+ symbols: [],
175
+ source,
176
+ };
177
+ };
178
+ let files = [];
179
+ try {
180
+ files = changedRequirementFiles(git, scope, base);
181
+ }
182
+ catch (error) {
183
+ notes.push(`Changed requirement documents could not be listed: ${error instanceof Error ? error.message : String(error)}`);
184
+ }
185
+ for (const { status, file } of files) {
186
+ const before = status === "A"
187
+ ? undefined
188
+ : parseEntity(tryGit(git, ["show", `${base}:${file}`]), file);
189
+ if (status === "D") {
190
+ if (before?.type === "req") {
191
+ drafts.set(before.id, draftFor(before, "retired", file));
192
+ }
193
+ continue;
194
+ }
195
+ const entity = parseEntity(currentContent(workspaceRoot, git, scope, file), file);
196
+ if (entity?.type !== "req")
197
+ continue;
198
+ const retired = RETIRED_STATUS.test(entity.status ?? "") &&
199
+ !RETIRED_STATUS.test(before?.status ?? "");
200
+ const change = status === "A" ? "new" : retired ? "retired" : "changed";
201
+ drafts.set(entity.id, draftFor(entity, change, file, before));
202
+ }
203
+ try {
204
+ const affected = await (deps.loadAffected ??
205
+ ((value) => loadAffectedForScope(workspaceRoot, value)))(scope);
206
+ for (const [id, symbols] of affected) {
207
+ const existing = drafts.get(id);
208
+ if (existing) {
209
+ drafts.set(id, { ...existing, symbols: [...symbols].sort() });
210
+ continue;
211
+ }
212
+ const entity = readRequirement(id);
213
+ if (!entity || RETIRED_STATUS.test(entity.status ?? ""))
214
+ continue;
215
+ drafts.set(id, {
216
+ ...draftFor(entity, "affected", `${REQUIREMENTS_DIR}/${id}.md`),
217
+ symbols: [...symbols].sort(),
218
+ });
219
+ }
220
+ }
221
+ catch (error) {
222
+ notes.push(`Requirements affected by code changes could not be computed: ${error instanceof Error ? error.message : String(error)}`);
223
+ }
224
+ let proof = new Map();
225
+ if (drafts.size > 0 && deps.loadProof) {
226
+ try {
227
+ proof = await deps.loadProof();
228
+ }
229
+ catch (error) {
230
+ notes.push(`Proof state is unavailable: ${error instanceof Error ? error.message : String(error)}`);
231
+ }
232
+ }
233
+ const unsynced = [...drafts.values()].filter((draft) => draft.change !== "retired" && !proof.has(draft.id));
234
+ if (proof.size > 0 && unsynced.length > 0) {
235
+ notes.push(`${unsynced.map((draft) => draft.id).join(", ")} ${unsynced.length === 1 ? "is" : "are"} not in the synced knowledge base yet; run kibi sync to see ${unsynced.length === 1 ? "its" : "their"} proof state.`);
236
+ }
237
+ const order = {
238
+ new: 0,
239
+ changed: 1,
240
+ affected: 2,
241
+ retired: 3,
242
+ };
243
+ const requirements = [...drafts.values()]
244
+ .map((draft) => {
245
+ const row = proof.get(draft.id);
246
+ return row ? { ...draft, proof: row } : draft;
247
+ })
248
+ .sort((left, right) => order[left.change] - order[right.change] ||
249
+ left.id.localeCompare(right.id));
250
+ const branch = tryGit(git, ["rev-parse", "--abbrev-ref", "HEAD"])?.trim();
251
+ return {
252
+ version: SIGNOFF_VERSION,
253
+ kind: "summary",
254
+ scope,
255
+ generatedAt: (deps.now ?? (() => new Date()))().toISOString(),
256
+ ...(branch && branch !== "HEAD" ? { branch } : {}),
257
+ requirements,
258
+ notes,
259
+ };
260
+ }
261
+ // implements REQ-kibi-signoff-views
262
+ /**
263
+ * Requirements whose implementing code the change edits, read from the
264
+ * `requirement_statement_unverified` impact diagnostics of `kibi check`.
265
+ */
266
+ export async function loadAffectedForScope(workspaceRoot, scope) {
267
+ const { analyzeChangedFileImpact } = await import("../public/impact/analyzer.js");
268
+ const result = await analyzeChangedFileImpact({
269
+ workspaceRoot,
270
+ ...(scope === "staged"
271
+ ? { staged: true }
272
+ : { includeWorkingTreeDiff: true }),
273
+ includeImpactDiagnostics: true,
274
+ });
275
+ const affected = new Map();
276
+ for (const diagnostic of result.impactDiagnostics) {
277
+ if (diagnostic.id !== "requirement_statement_unverified")
278
+ continue;
279
+ const evidence = diagnostic.evidence;
280
+ const id = typeof evidence?.requirement === "string"
281
+ ? evidence.requirement
282
+ : diagnostic.entityId;
283
+ if (!id)
284
+ continue;
285
+ const symbols = Array.isArray(evidence?.symbols)
286
+ ? evidence.symbols.filter((symbol) => typeof symbol === "string")
287
+ : [];
288
+ affected.set(id, symbols);
289
+ }
290
+ return affected;
291
+ }
292
+ // implements REQ-kibi-signoff-views
293
+ /**
294
+ * The mid-work decision questions, numbered, with the recorded requirement a
295
+ * question would change and the recorded rules it conflicts with quoted from
296
+ * the knowledge base rather than from the agent's memory.
297
+ */
298
+ export function collectSignoffQuestions(workspaceRoot, inputs, now = () => new Date()) {
299
+ const readRequirement = workspaceRequirements(workspaceRoot);
300
+ const notes = [];
301
+ const questions = inputs.map((input, index) => {
302
+ const current = input.id ? readRequirement(input.id) : undefined;
303
+ const conflicts = [];
304
+ for (const id of input.conflictsWith ?? []) {
305
+ const entity = readRequirement(id);
306
+ if (entity)
307
+ conflicts.push(statementRef(entity));
308
+ else
309
+ notes.push(`Question ${index + 1}: ${id} is not a recorded requirement.`);
310
+ }
311
+ return {
312
+ ...input,
313
+ number: index + 1,
314
+ ...(current ? { current: statementRef(current) } : {}),
315
+ conflicts,
316
+ };
317
+ });
318
+ return {
319
+ version: SIGNOFF_VERSION,
320
+ kind: "questions",
321
+ generatedAt: now().toISOString(),
322
+ questions,
323
+ notes,
324
+ };
325
+ }
326
+ const FIRMNESS_VALUES = new Set(["firm", "default", "exploratory"]);
327
+ function optionalString(record, key, where) {
328
+ const value = record[key];
329
+ if (value === undefined || value === null)
330
+ return undefined;
331
+ if (typeof value !== "string") {
332
+ throw new Error(`${where}.${key} must be a string`);
333
+ }
334
+ return value.trim() === "" ? undefined : value.trim();
335
+ }
336
+ function optionalStrings(record, key, where) {
337
+ const value = record[key];
338
+ if (value === undefined || value === null)
339
+ return undefined;
340
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
341
+ throw new Error(`${where}.${key} must be an array of strings`);
342
+ }
343
+ return value.map((item) => item.trim()).filter(Boolean);
344
+ }
345
+ // implements REQ-kibi-signoff-views
346
+ /**
347
+ * Parse the questions input: `{"questions":[{statement, id?, title?, quote?,
348
+ * firmness?, governs?, conflictsWith?, question?, answer?}]}` or a bare array.
349
+ */
350
+ export function parseSignoffQuestionsInput(raw) {
351
+ let parsed;
352
+ try {
353
+ parsed = JSON.parse(raw);
354
+ }
355
+ catch (error) {
356
+ throw new Error(`Questions input is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
357
+ }
358
+ const list = Array.isArray(parsed)
359
+ ? parsed
360
+ : parsed !== null && typeof parsed === "object"
361
+ ? parsed.questions
362
+ : undefined;
363
+ if (!Array.isArray(list) || list.length === 0) {
364
+ throw new Error('Questions input must be {"questions":[...]} or an array with at least one question');
365
+ }
366
+ return list.map((item, index) => {
367
+ const where = `questions[${index}]`;
368
+ if (item === null || typeof item !== "object" || Array.isArray(item)) {
369
+ throw new Error(`${where} must be an object`);
370
+ }
371
+ const record = item;
372
+ const statement = optionalString(record, "statement", where);
373
+ if (!statement)
374
+ throw new Error(`${where}.statement is required`);
375
+ const firmness = optionalString(record, "firmness", where);
376
+ if (firmness !== undefined && !FIRMNESS_VALUES.has(firmness)) {
377
+ throw new Error(`${where}.firmness must be firm, default or exploratory`);
378
+ }
379
+ const id = optionalString(record, "id", where);
380
+ const title = optionalString(record, "title", where);
381
+ const quote = optionalString(record, "quote", where);
382
+ const governs = optionalStrings(record, "governs", where);
383
+ const conflictsWith = optionalStrings(record, "conflictsWith", where);
384
+ const question = optionalString(record, "question", where);
385
+ const answer = optionalString(record, "answer", where);
386
+ return {
387
+ statement: flat(statement),
388
+ ...(id ? { id } : {}),
389
+ ...(title ? { title } : {}),
390
+ ...(quote ? { quote } : {}),
391
+ ...(firmness ? { firmness: firmness } : {}),
392
+ ...(governs?.length ? { governs } : {}),
393
+ ...(conflictsWith?.length ? { conflictsWith } : {}),
394
+ ...(question ? { question } : {}),
395
+ ...(answer ? { answer } : {}),
396
+ };
397
+ });
398
+ }
@@ -0,0 +1,4 @@
1
+ import { type SignoffView } from "./model.js";
2
+ /** A signoff view as a self-contained HTML page with no network assets. */
3
+ export declare function renderSignoffHtml(view: SignoffView): string;
4
+ //# sourceMappingURL=html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../src/signoff/html.ts"],"names":[],"mappings":"AAMA,OAAO,EAOL,KAAK,WAAW,EAQjB,MAAM,YAAY,CAAC;AA6OpB,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,CAoC3D"}
@@ -0,0 +1,210 @@
1
+ import { KIBI_BRAND, renderKibiFaviconDataUri, renderKibiLogo, } from "../report/brand.js";
2
+ import { REPORT_CSP } from "../report/html-report.js";
3
+ import { SIGNOFF_CHANGE_LABELS, approvalNumbers, approvalSentence, awaitsApproval, proofLabel, questionKind, questionOpen, statementSentenceDiff, } from "./model.js";
4
+ function escapeHtml(value) {
5
+ return String(value ?? "")
6
+ .replaceAll("&", "&")
7
+ .replaceAll("<", "&lt;")
8
+ .replaceAll(">", "&gt;")
9
+ .replaceAll('"', "&quot;")
10
+ .replaceAll("'", "&#39;");
11
+ }
12
+ function styles() {
13
+ return `
14
+ :root{--bg:${KIBI_BRAND.deepCarbon};--panel:${KIBI_BRAND.panel};--text:${KIBI_BRAND.snow};--muted:${KIBI_BRAND.mist};--rail:${KIBI_BRAND.rail};--accent:${KIBI_BRAND.ice};--signal:${KIBI_BRAND.signal};--ok:${KIBI_BRAND.success};--warn:${KIBI_BRAND.warning};--bad:${KIBI_BRAND.danger}}
15
+ @media (prefers-color-scheme: light){:root{--bg:#f4f6f8;--panel:#ffffff;--text:#1d1e23;--muted:#4f5b66;--rail:#d5dde3;--accent:#1f5f99;--signal:#1f5f99;--ok:#1e7a4f;--warn:#8a5a00;--bad:#b3261e}}
16
+ *{box-sizing:border-box}
17
+ body{margin:0;background:var(--bg);color:var(--text);font:15px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif}
18
+ main{max-width:920px;margin:0 auto;padding:24px 16px 48px}
19
+ header.page{display:flex;gap:12px;align-items:center;margin-bottom:8px}
20
+ header.page svg{width:36px;height:36px;flex:none}
21
+ h1{font-size:22px;margin:0}
22
+ .meta{color:var(--muted);font-size:13px;margin:0 0 20px}
23
+ h2{font-size:16px;margin:28px 0 10px;color:var(--accent)}
24
+ .card{background:var(--panel);border:1px solid var(--rail);border-radius:10px;padding:14px 16px;margin:10px 0}
25
+ .card--answered{opacity:.6}
26
+ .card__head{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}
27
+ .num{font-weight:700;color:var(--muted)}
28
+ .id{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;color:var(--accent)}
29
+ .title{font-weight:600}
30
+ .badge{font-size:12px;padding:1px 8px;border-radius:999px;border:1px solid var(--rail);color:var(--muted)}
31
+ .badge--firm{border-color:var(--warn);color:var(--warn)}
32
+ .badge--exploratory{border-color:var(--signal);color:var(--signal)}
33
+ .badge--proven{border-color:var(--ok);color:var(--ok)}
34
+ .badge--answered{border-color:var(--ok);color:var(--ok)}
35
+ .badge--approval{border-color:var(--signal);color:var(--signal);font-weight:600}
36
+ .callout{margin:12px 0 4px;padding:10px 14px;border:1px solid var(--signal);border-radius:8px;color:var(--text)}
37
+ blockquote{margin:10px 0;padding:8px 12px;border-left:3px solid var(--accent);background:rgba(127,127,127,.08);border-radius:0 6px 6px 0}
38
+ .diff{display:grid;gap:6px;margin:10px 0}
39
+ .diff blockquote{margin:0}
40
+ .diff .was{border-left-color:var(--bad);text-decoration:line-through;text-decoration-color:rgba(240,113,120,.6)}
41
+ .diff .now{border-left-color:var(--ok)}
42
+ .diff ins{text-decoration:none;background:rgba(61,186,128,.18);color:var(--text);border-radius:3px;padding:0 2px}
43
+ .diff del{color:var(--muted);text-decoration-color:var(--bad);background:rgba(240,113,120,.12);border-radius:3px;padding:0 2px}
44
+ .label{font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em}
45
+ dl{display:grid;grid-template-columns:max-content 1fr;gap:4px 12px;margin:8px 0 0;font-size:14px}
46
+ dt{color:var(--muted)}
47
+ dd{margin:0;overflow-wrap:anywhere}
48
+ .conflict{color:var(--bad)}
49
+ .notes{color:var(--muted);font-size:13px}
50
+ .empty{color:var(--muted)}
51
+ footer{margin-top:28px;color:var(--muted);font-size:13px}
52
+ `;
53
+ }
54
+ function firmnessBadge(firmness) {
55
+ const value = firmness ?? "default";
56
+ return `<span class="badge badge--${escapeHtml(value)}">${escapeHtml(value)}</span>`;
57
+ }
58
+ function row(label, value) {
59
+ return `<dt>${escapeHtml(label)}</dt><dd>${value}</dd>`;
60
+ }
61
+ function statementBlock(statement, previous, previousLabel = "Was", currentLabel = "Now") {
62
+ if (previous === undefined) {
63
+ return `<blockquote>${escapeHtml(statement)}</blockquote>`;
64
+ }
65
+ const diff = statementSentenceDiff(previous, statement);
66
+ if (diff !== undefined) {
67
+ const text = diff
68
+ .map((part) => part.change === "added"
69
+ ? `<ins>${escapeHtml(part.text)}</ins>`
70
+ : part.change === "removed"
71
+ ? `<del>${escapeHtml(part.text)}</del>`
72
+ : escapeHtml(part.text))
73
+ .join(" ");
74
+ return `<div class="diff"><span class="label">${escapeHtml(currentLabel)}, with the changed sentences marked</span><blockquote class="now">${text}</blockquote></div>`;
75
+ }
76
+ return `<div class="diff"><span class="label">${escapeHtml(previousLabel)}</span><blockquote class="was">${escapeHtml(previous)}</blockquote><span class="label">${escapeHtml(currentLabel)}</span><blockquote class="now">${escapeHtml(statement)}</blockquote></div>`;
77
+ }
78
+ function requirementCard(requirement, number) {
79
+ const details = [];
80
+ const origin = [
81
+ requirement.originKind,
82
+ requirement.originRef ? `“${requirement.originRef}”` : undefined,
83
+ requirement.approvedBy
84
+ ? `approved by ${requirement.approvedBy}`
85
+ : undefined,
86
+ ].filter(Boolean);
87
+ if (origin.length > 0)
88
+ details.push(row("Origin", escapeHtml(origin.join(", "))));
89
+ for (const superseded of requirement.supersedes) {
90
+ details.push(row("Replaces", `<span class="id">${escapeHtml(superseded.id)}</span> ${firmnessBadge(superseded.firmness)} ${escapeHtml(superseded.statement)}`));
91
+ }
92
+ if (requirement.symbols.length > 0) {
93
+ details.push(row("Changed code", escapeHtml(requirement.symbols.join(", "))));
94
+ }
95
+ if (requirement.change !== "retired") {
96
+ details.push(row("Scenarios", requirement.scenarios.length === 0
97
+ ? '<span class="empty">none yet</span>'
98
+ : requirement.scenarios
99
+ .map((scenario) => `<span class="id">${escapeHtml(scenario.id)}</span>${scenario.title === scenario.id ? "" : ` ${escapeHtml(scenario.title)}`}`)
100
+ .join("<br>")));
101
+ const proven = requirement.proof?.gate === "proven";
102
+ details.push(row("Proof", `<span class="badge${proven ? " badge--proven" : ""}">${escapeHtml(proofLabel(requirement.proof))}</span>`));
103
+ for (const conflict of requirement.proof?.contradictions ?? []) {
104
+ details.push(row("Contradiction", `<span class="conflict">${escapeHtml(conflict)}</span>`));
105
+ }
106
+ }
107
+ return `<article class="card" id="${escapeHtml(requirement.id)}">
108
+ <div class="card__head"><span class="num">${number}.</span><span class="id">${escapeHtml(requirement.id)}</span>${firmnessBadge(requirement.firmness)}<span class="title">${escapeHtml(requirement.title)}</span>${awaitsApproval(requirement) ? '<span class="badge badge--approval">awaits your approval</span>' : ""}</div>
109
+ ${statementBlock(requirement.statement, requirement.previousStatement)}
110
+ <dl>${details.join("")}</dl>
111
+ </article>`;
112
+ }
113
+ function summaryBody(summary) {
114
+ const order = [
115
+ "new",
116
+ "changed",
117
+ "affected",
118
+ "retired",
119
+ ];
120
+ const sections = [];
121
+ const approvals = approvalNumbers(order.flatMap((change) => summary.requirements.filter((item) => item.change === change)));
122
+ if (approvals.length > 0) {
123
+ sections.push(`<p class="callout">${escapeHtml(approvalSentence(approvals))} Reply by number, for example “approve 1”.</p>`);
124
+ }
125
+ let number = 0;
126
+ for (const change of order) {
127
+ const rows = summary.requirements.filter((item) => item.change === change);
128
+ if (rows.length === 0)
129
+ continue;
130
+ sections.push(`<h2>${escapeHtml(SIGNOFF_CHANGE_LABELS[change])} (${rows.length})</h2>`);
131
+ for (const item of rows) {
132
+ number += 1;
133
+ sections.push(requirementCard(item, number));
134
+ }
135
+ }
136
+ if (sections.length === 0) {
137
+ sections.push('<p class="empty">This change adds, rewrites or affects no requirement.</p>');
138
+ }
139
+ return sections.join("\n");
140
+ }
141
+ function questionCard(question) {
142
+ const kind = questionKind(question);
143
+ const title = question.title ?? question.current?.title ?? question.id;
144
+ if (!questionOpen(question)) {
145
+ return `<article class="card card--answered"><div class="card__head"><span class="num">${question.number}.</span><span class="title">${escapeHtml(kind)}${title ? `: ${escapeHtml(title)}` : ""}</span><span class="badge badge--answered">answered: ${escapeHtml(question.answer)}</span></div></article>`;
146
+ }
147
+ const details = [];
148
+ if (question.quote)
149
+ details.push(row("Your words", escapeHtml(`“${question.quote}”`)));
150
+ if (question.governs?.length)
151
+ details.push(row("Governs", escapeHtml(question.governs.join(", "))));
152
+ for (const conflict of question.conflicts) {
153
+ details.push(row("Conflicts with", `<span class="id conflict">${escapeHtml(conflict.id)}</span> ${firmnessBadge(conflict.firmness)} ${escapeHtml(conflict.statement)}`));
154
+ }
155
+ if (question.question)
156
+ details.push(row("Question", escapeHtml(question.question)));
157
+ return `<article class="card" id="q${question.number}">
158
+ <div class="card__head"><span class="num">${question.number}.</span><span class="title">${escapeHtml(kind)}${title ? `: ${escapeHtml(title)}` : ""}</span>${question.firmness ? `<span class="badge badge--${escapeHtml(question.firmness)}">would be ${escapeHtml(question.firmness)}</span>` : ""}</div>
159
+ ${statementBlock(question.statement, question.current?.statement, "Recorded now", "Proposed")}
160
+ <dl>${details.join("")}</dl>
161
+ </article>`;
162
+ }
163
+ function questionsBody(view) {
164
+ const open = view.questions.filter(questionOpen);
165
+ const answered = view.questions.filter((question) => !questionOpen(question));
166
+ const parts = [`<h2>Open (${open.length})</h2>`, ...open.map(questionCard)];
167
+ if (open.length > 0) {
168
+ parts.push('<p class="notes">Answer in the chat by number, for example “1 yes, 2 exploratory, 3 reword: …”.</p>');
169
+ }
170
+ if (answered.length > 0) {
171
+ parts.push(`<h2>Answered (${answered.length})</h2>`, ...answered.map(questionCard));
172
+ }
173
+ return parts.join("\n");
174
+ }
175
+ // implements REQ-kibi-signoff-views
176
+ /** A signoff view as a self-contained HTML page with no network assets. */
177
+ export function renderSignoffHtml(view) {
178
+ const heading = view.kind === "summary" ? "Requirements signoff" : "Decisions to confirm";
179
+ const meta = view.kind === "summary"
180
+ ? [
181
+ view.branch ? `Branch ${view.branch}` : undefined,
182
+ view.scope === "staged" ? "staged changes" : "working tree",
183
+ `generated ${view.generatedAt}`,
184
+ ]
185
+ : [`generated ${view.generatedAt}`];
186
+ const body = view.kind === "summary" ? summaryBody(view) : questionsBody(view);
187
+ const notes = view.notes.length
188
+ ? `<footer>${view.notes.map((note) => `<p class="notes">${escapeHtml(note)}</p>`).join("")}</footer>`
189
+ : "";
190
+ return `<!doctype html>
191
+ <html lang="en">
192
+ <head>
193
+ <meta charset="utf-8">
194
+ <meta name="viewport" content="width=device-width, initial-scale=1">
195
+ <meta http-equiv="Content-Security-Policy" content="${REPORT_CSP}">
196
+ <title>Kibi ${escapeHtml(heading.toLowerCase())}</title>
197
+ <link rel="icon" href="${renderKibiFaviconDataUri()}">
198
+ <style>${styles()}</style>
199
+ </head>
200
+ <body>
201
+ <main>
202
+ <header class="page">${renderKibiLogo()}<h1>${escapeHtml(heading)}</h1></header>
203
+ <p class="meta">${escapeHtml(meta.filter(Boolean).join(" · "))}</p>
204
+ ${body}
205
+ ${notes}
206
+ </main>
207
+ </body>
208
+ </html>
209
+ `;
210
+ }
@@ -0,0 +1,20 @@
1
+ import { type SignoffQuestions, type SignoffSummary, type SignoffView } from "./model.js";
2
+ /** Rows shown in chat before the rest is left to the HTML view. */
3
+ export declare const SIGNOFF_MARKDOWN_LIMIT = 15;
4
+ /** The end-of-work summary as Markdown, without the fence. */
5
+ export declare function renderSummaryMarkdownBody(summary: SignoffSummary, limit?: number): string;
6
+ /** The mid-work decision questions as Markdown, without the fence. */
7
+ export declare function renderQuestionsMarkdownBody(view: SignoffQuestions): string;
8
+ /**
9
+ * A signoff view as plain Markdown for a chat reply or a messaging tool
10
+ * (headings, quotes and lists that any Markdown surface renders), with no
11
+ * fence for a host renderer to find.
12
+ */
13
+ export declare function renderSignoffChatMarkdown(view: SignoffView): string;
14
+ /**
15
+ * A signoff view as the fenced block agents paste into chat. Host renderers
16
+ * (the Claude Code display hook, the OpenCode TUI) key on the fence's info
17
+ * string; hosts without one show it as a code block, so they get `chat`.
18
+ */
19
+ export declare function renderSignoffMarkdown(view: SignoffView): string;
20
+ //# sourceMappingURL=markdown.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/signoff/markdown.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,gBAAgB,EAErB,KAAK,cAAc,EACnB,KAAK,WAAW,EAQjB,MAAM,YAAY,CAAC;AAEpB,mEAAmE;AACnE,eAAO,MAAM,sBAAsB,KAAK,CAAC;AA6HzC,8DAA8D;AAC9D,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,cAAc,EACvB,KAAK,SAAyB,GAC7B,MAAM,CAmCR;AAGD,sEAAsE;AACtE,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,gBAAgB,GAAG,MAAM,CA8C1E;AAGD;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,CAInE;AAGD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,CAM/D"}