karajan-code 4.21.0 → 4.23.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 (54) hide show
  1. package/README.md +6 -3
  2. package/package.json +5 -2
  3. package/packages/hu-board/public/app.js +1 -0
  4. package/packages/hu-board/public/index.html +2 -0
  5. package/packages/hu-board/public/styles.css +22 -0
  6. package/packages/hu-board/public/utils/governance-view.js +149 -0
  7. package/packages/hu-board/src/auth.js +29 -1
  8. package/packages/hu-board/src/routes/governance.js +140 -0
  9. package/packages/hu-board/src/server.js +2 -0
  10. package/scripts/install.js +4 -3
  11. package/scripts/postinstall.js +4 -3
  12. package/scripts/toml-value.js +18 -0
  13. package/src/audit/basal-cost.js +4 -0
  14. package/src/audit/dead-exports.js +52 -1
  15. package/src/audit/deterministic-summary.js +18 -3
  16. package/src/audit/member-reachability.js +158 -0
  17. package/src/audit/osv-findings.js +1 -0
  18. package/src/checks/ai-trash.js +1 -1
  19. package/src/checks/mcp-health.js +1 -1
  20. package/src/checks/native-build.js +2 -2
  21. package/src/checks/release-check.js +62 -2
  22. package/src/claims/cross-check.js +86 -0
  23. package/src/claims/extract.js +56 -0
  24. package/src/claims/turn.js +54 -0
  25. package/src/cli/advanced-commands.js +1 -1
  26. package/src/cli/register-meta.js +66 -5
  27. package/src/commands/claims.js +73 -0
  28. package/src/commands/hu.js +3 -1
  29. package/src/commands/init.js +1 -1
  30. package/src/commands/policy.js +84 -3
  31. package/src/commands/privacy.js +6 -2
  32. package/src/commands/resume.js +4 -0
  33. package/src/commands/review-gate.js +34 -9
  34. package/src/commands/steward.js +146 -0
  35. package/src/config/defaults.js +6 -1
  36. package/src/environment/playbook.js +2 -1
  37. package/src/guards/duplicate-members.js +86 -0
  38. package/src/harden/sentinel-hooks.js +188 -22
  39. package/src/harden/workflow-engine.js +8 -2
  40. package/src/harden/workflow-templates.js +38 -1
  41. package/src/policy/exceptions.js +14 -4
  42. package/src/policy/report.js +99 -0
  43. package/src/privacy/scan.js +23 -1
  44. package/src/review/card-first.js +4 -1
  45. package/src/review/one-shot-review.js +10 -4
  46. package/src/review/parser.js +9 -0
  47. package/src/review/sonar-pregate.js +38 -3
  48. package/src/review/tests-with-code.js +12 -1
  49. package/src/review/unparseable-verdict.js +50 -0
  50. package/src/roles/audit-role.js +19 -1
  51. package/src/sonar/scanner.js +34 -8
  52. package/src/steward/invariants.js +190 -0
  53. package/src/steward/phantom-coverage.js +137 -0
  54. package/src/steward/proposed-work.js +68 -0
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Phantom coverage (KJC-TSK-0800, epic KJC-PCS-0081) — a phantom test is
3
+ * worse than no test: it adds to the count, feels like a net, and covers
4
+ * nothing. TWO detectors, because one is not enough: the call graph catches
5
+ * a unit test that only exercises unreachable members, and LITERAL crossing
6
+ * catches the case the graph cannot see — GREBLA's hierarchy.spec called no
7
+ * dead method, it looked for an aria-label that only existed inside one.
8
+ * Both inherit the reachability perimeter: a NOT OBSERVABLE analysis makes
9
+ * the detector not observable — no test is accused on an unreliable basis.
10
+ */
11
+ import { parse } from "@babel/parser";
12
+
13
+ const parseLoose = (source, file) =>
14
+ parse(source, {
15
+ sourceType: "unambiguous", allowReturnOutsideFunction: true,
16
+ plugins: [...(/\.(ts|tsx|mts|cts)$/.test(file) ? ["typescript"] : []), ...(/\.(tsx|jsx)$/.test(file) || !/\.(ts|mts|cts)$/.test(file) ? ["jsx"] : []), "decorators"],
17
+ });
18
+
19
+ function walk(node, visit) {
20
+ if (!node || typeof node.type !== "string") return;
21
+ visit(node);
22
+ for (const key of Object.keys(node)) {
23
+ const child = node[key];
24
+ if (Array.isArray(child)) child.forEach((c) => walk(c, visit));
25
+ else if (child && typeof child.type === "string") walk(child, visit);
26
+ }
27
+ }
28
+
29
+ const notObservable = (sourceAnalysis) => {
30
+ if (!sourceAnalysis?.observable) return sourceAnalysis?.reason || "the reachability analysis is not observable";
31
+ const classes = (sourceAnalysis.classes || []).filter((c) => c.observable);
32
+ if (classes.length === 0) return "no class in the source is observable — the analysis cannot vouch for anything";
33
+ return null;
34
+ };
35
+ const observableClasses = (a) => (a.classes || []).filter((c) => c.observable);
36
+ const bareName = (slot) => slot.replace(/^static /, "");
37
+
38
+ /**
39
+ * Unit detector: of the test's member CALLS that belong to an analyzed class,
40
+ * do all of them land on unreachable members? One live call absolves.
41
+ * @returns {{observable: boolean, reason?: string, phantoms: Array<{member: string, className: string}>}}
42
+ */
43
+ export function detectPhantomUnit({ sourceAnalysis, testSource, file = "<test>" }) {
44
+ const why = notObservable(sourceAnalysis);
45
+ if (why) return { observable: false, reason: `not observable: ${why}`, phantoms: [] };
46
+ let tree;
47
+ try { tree = parseLoose(testSource, file); } catch (err) {
48
+ return { observable: false, reason: `the test could not be parsed (${err.message})`, phantoms: [] };
49
+ }
50
+ // Calls grouped by RECEIVER (weak binding, reviewer's catch): a receiver is
51
+ // tied to a class only when EVERY member it calls belongs to that class —
52
+ // mixing two objects under one name must not get a test accused.
53
+ const byReceiver = new Map();
54
+ walk(tree, (n) => {
55
+ if (n.type !== "CallExpression" || n.callee?.type !== "MemberExpression" || n.callee.computed || n.callee.property?.type !== "Identifier") return;
56
+ const recv = n.callee.object?.type === "Identifier" ? n.callee.object.name : n.callee.object?.type === "ThisExpression" ? "this" : "(expr)";
57
+ (byReceiver.get(recv) ?? byReceiver.set(recv, new Set()).get(recv)).add(n.callee.property.name);
58
+ });
59
+ const phantoms = [];
60
+ for (const c of observableClasses(sourceAnalysis)) {
61
+ const members = new Set(c.memberNames.map(bareName));
62
+ const dead = new Set(c.unreachable.map((u) => bareName(u.name)));
63
+ for (const [, names] of byReceiver) {
64
+ const touched = [...names].filter((n) => members.has(n));
65
+ // bound to this class, and everything it touches is dead → phantom;
66
+ // one live call — or a call outside the class — absolves the receiver.
67
+ if (touched.length > 0 && touched.length === names.size && touched.every((n) => dead.has(n))) {
68
+ for (const n of touched) phantoms.push({ member: n, className: c.name, file });
69
+ }
70
+ }
71
+ }
72
+ return { observable: true, phantoms };
73
+ }
74
+
75
+ const literalsOf = (tree, minLength) => {
76
+ const out = [];
77
+ walk(tree, (n) => {
78
+ if (n.type === "StringLiteral" && n.value.trim().length >= minLength) out.push({ value: n.value, line: n.loc.start.line });
79
+ if (n.type === "TemplateElement" && n.value.cooked && n.value.cooked.trim().length >= minLength) out.push({ value: n.value.cooked, line: n.loc.start.line });
80
+ });
81
+ return out;
82
+ };
83
+
84
+ /**
85
+ * E2E detector: literals the test looks for that ONLY exist inside
86
+ * unreachable spans of the source. A literal also present in reachable code
87
+ * is never reported — that test does cover something.
88
+ * @returns {{observable: boolean, reason?: string, phantoms: Array<{literal: string, line: number}>}}
89
+ */
90
+ export function detectPhantomE2E({ sourceText, sourceAnalysis, testSource, file = "<spec>", minLength = 4 }) {
91
+ const why = notObservable(sourceAnalysis);
92
+ if (why) return { observable: false, reason: `not observable: ${why}`, phantoms: [] };
93
+ let sourceTree, testTree;
94
+ try {
95
+ sourceTree = parseLoose(sourceText, sourceAnalysis.file || "<source>");
96
+ testTree = parseLoose(testSource, file);
97
+ } catch (err) {
98
+ return { observable: false, reason: `could not be parsed (${err.message})`, phantoms: [] };
99
+ }
100
+ const deadSpans = observableClasses(sourceAnalysis).flatMap((c) => c.unreachable.map((u) => [u.line, u.endLine]));
101
+ const inDead = (line) => deadSpans.some(([a, b]) => line >= a && line <= b);
102
+ const onlyInDead = new Map(); // literal → line where it lives
103
+ for (const lit of literalsOf(sourceTree, minLength)) {
104
+ if (inDead(lit.line)) { if (!onlyInDead.has(lit.value)) onlyInDead.set(lit.value, lit.line); }
105
+ else onlyInDead.set(lit.value, -1); // seen reachable — poisoned, never reportable
106
+ }
107
+ const wanted = new Set(literalsOf(testTree, minLength).map((l) => l.value));
108
+ const phantoms = [...wanted]
109
+ .filter((v) => onlyInDead.get(v) !== undefined && onlyInDead.get(v) !== -1)
110
+ .map((v) => ({ literal: v, line: onlyInDead.get(v), file }));
111
+ return { observable: true, phantoms };
112
+ }
113
+
114
+ /**
115
+ * The temporal signature — zero static analysis, and it would have been
116
+ * enough for GREBLA: a bug gets fixed or reverted; a phantom fails the SAME
117
+ * way indefinitely because it proves something that no longer exists.
118
+ * kj holds no per-test history, so the source is INJECTED and said:
119
+ * failures = [{test, reason, at}] from whoever owns the CI history.
120
+ */
121
+ export function assessTemporalSignature({ failures, thresholdDays = 7 } = {}) {
122
+ if (!Array.isArray(failures)) {
123
+ return { observable: false, reason: "no per-test failure history handed in — kj holds none; inject it from the CI that owns it", suspects: [] };
124
+ }
125
+ const byTest = new Map();
126
+ for (const f of failures) (byTest.get(f.test) ?? byTest.set(f.test, []).get(f.test)).push(f);
127
+ const suspects = [];
128
+ for (const [test, rows] of byTest) {
129
+ const reasons = new Set(rows.map((r) => String(r.reason || "").trim().toLowerCase()));
130
+ if (reasons.size !== 1) continue; // changing reasons look like a bug being worked
131
+ const times = rows.map((r) => Date.parse(r.at)).filter(Number.isFinite);
132
+ if (times.length < 2) continue;
133
+ const days = Math.floor((Math.max(...times) - Math.min(...times)) / 86_400_000);
134
+ if (days >= thresholdDays) suspects.push({ test, reason: rows[0].reason, days, firstAt: new Date(Math.min(...times)).toISOString(), lastAt: new Date(Math.max(...times)).toISOString() });
135
+ }
136
+ return { observable: true, suspects };
137
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Proposed work (STW-D, KJC-TSK-0792, epic KJC-PCS-0081) — every broken
3
+ * invariant becomes a CARD in the channel the Brain already consumes, as a
4
+ * PROPOSAL: evidence, since-when, and a remedy plan that nothing executes
5
+ * without review (the user's decision, 22-aug: an unreviewed generated plan
6
+ * can do more harm than the decay itself). The same break UPDATES its card —
7
+ * a daily sweep must never flood the board with twins — and a green
8
+ * invariant resolves it with the green evidence.
9
+ *
10
+ * kj only writes the board it owns (hu-board). planning-game and external
11
+ * boards are never mirrored (KJC-TSK-0684: a half-empty parallel board is
12
+ * worse than none): there the sweep reports synced:false LOUDLY, and the
13
+ * Sentinel's inevitable notice (STW-C) pushes the host agent — who does
14
+ * have the board's MCP — to card them.
15
+ */
16
+ import fs from "node:fs";
17
+ import path from "node:path";
18
+ import { addHu, updateHu, updateHuStatus } from "../plan/plan-hu-ops.js";
19
+ import { savePlan } from "../plan/plan-store.js";
20
+ import { backlogPlan } from "../commands/hu.js";
21
+
22
+ const mapPath = (projectDir) => path.join(projectDir, ".karajan", "steward", "cards.json");
23
+ const readMap = (projectDir) => { try { return JSON.parse(fs.readFileSync(mapPath(projectDir), "utf8")); } catch { return {}; } };
24
+
25
+ const cardText = (r, brokenSince) => [
26
+ `Steward invariant BROKEN since ${brokenSince}.`,
27
+ `Evidence: ${r.evidence || "(none recorded)"}`,
28
+ `Proposed remedy plan: ${r.remedy || r.renew || "kj steward sweep"}.`,
29
+ "This plan is a PROPOSAL — review before executing: nothing in it runs unreviewed.",
30
+ ].join("\n");
31
+
32
+ /**
33
+ * @returns {Promise<{synced: boolean, reason?: string, created?: number, updated?: number, resolved?: number}>}
34
+ */
35
+ export async function syncProposedWork({ projectDir, config = {}, results = [], sweptAt }) {
36
+ const backend = config.state_backend || "hu-board";
37
+ if (backend !== "hu-board") {
38
+ return { synced: false, reason: `the board lives in ${backend} — kj never mirrors a board it does not own; card the broken invariants through your agent's board tools` };
39
+ }
40
+ const map = readMap(projectDir);
41
+ const broken = results.filter((r) => r.verdict === "broken");
42
+ const okAgain = results.filter((r) => r.verdict === "ok" && map[r.id]);
43
+ if (broken.length === 0 && okAgain.length === 0) return { synced: true, created: 0, updated: 0, resolved: 0 };
44
+
45
+ const plan = await backlogPlan(projectDir);
46
+ let created = 0, updated = 0, resolved = 0;
47
+ for (const r of broken) {
48
+ const known = map[r.id];
49
+ if (known) {
50
+ updateHu(plan, known.huId, { scope: cardText(r, known.brokenSince) });
51
+ updated += 1;
52
+ } else {
53
+ const hu = addHu(plan, { title: `steward: ${r.id} broken — proposed remedy (review before executing)`, scope: cardText(r, sweptAt), created_by: "steward" });
54
+ map[r.id] = { huId: hu.id, brokenSince: sweptAt };
55
+ created += 1;
56
+ }
57
+ }
58
+ for (const r of okAgain) {
59
+ updateHu(plan, map[r.id].huId, { scope: `Resolved: the invariant is green again (${r.evidence || "no evidence text"}) — validation stays with the user.` });
60
+ updateHuStatus(plan, map[r.id].huId, "done");
61
+ delete map[r.id];
62
+ resolved += 1;
63
+ }
64
+ await savePlan(projectDir, plan);
65
+ fs.mkdirSync(path.dirname(mapPath(projectDir)), { recursive: true });
66
+ fs.writeFileSync(mapPath(projectDir), `${JSON.stringify(map, null, 2)}\n`);
67
+ return { synced: true, created, updated, resolved };
68
+ }