@azure-id/orc 2.0.2 → 2.1.1
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/CHANGELOG.md +455 -0
- package/README-id.md +52 -23
- package/README.md +36 -20
- package/bin/build-agents.js +205 -206
- package/bin/clear-logs.js +620 -0
- package/bin/cli.js +47417 -46420
- package/bin/fix.js +459 -0
- package/bin/gotcha.js +288 -8
- package/bin/habit.js +1538 -1453
- package/bin/mockrun-catalog.js +3 -0
- package/bin/onboarding-content.js +2 -1
- package/bin/pricing.json +28 -21
- package/bin/trace-write.js +869 -657
- package/bin/verify-contracts.js +5552 -5471
- package/bin/verify-package.js +716 -705
- package/bin/webui/api.js +125 -6
- package/bin/webui/css/panels/hookui.css +79 -0
- package/bin/webui/fixtures/behaviour.js +30 -0
- package/bin/webui/fixtures/extra.js +2036 -2036
- package/bin/webui/fixtures/flow.js +81 -81
- package/bin/webui/fixtures/hookui.js +506 -431
- package/bin/webui/fixtures/index.js +32 -3
- package/bin/webui/fixtures/knowledge.js +2 -2
- package/bin/webui/fixtures/maintenance.js +123 -42
- package/bin/webui/fixtures/rules.js +1007 -1002
- package/bin/webui/fixtures/settings.js +6 -7
- package/bin/webui/fixtures/stats.js +9 -6
- package/bin/webui/fixtures/wait.js +98 -97
- package/bin/webui/i18n/en/behaviour.json +13 -0
- package/bin/webui/i18n/en/hookui.json +290 -194
- package/bin/webui/i18n/en/maintenance.json +71 -52
- package/bin/webui/i18n/en/settings.json +60 -60
- package/bin/webui/i18n/id/behaviour.json +13 -0
- package/bin/webui/i18n/id/hookui.json +290 -194
- package/bin/webui/i18n/id/maintenance.json +71 -52
- package/bin/webui/i18n/id/settings.json +60 -60
- package/bin/webui/js/05-banners.js +181 -172
- package/bin/webui/js/06-edit.js +193 -183
- package/bin/webui/js/panels/behaviour.js +77 -12
- package/bin/webui/js/panels/hookui.js +2196 -1691
- package/bin/webui/js/panels/maintenance.js +336 -235
- package/bin/webui/js/panels/settings.js +726 -719
- package/mock-run/INDEX.md +1 -0
- package/mock-run/a-normal-day.md +2 -2
- package/mock-run/extra-slots.md +177 -177
- package/mock-run/habits.md +1 -1
- package/mock-run/orc-aftermath.md +392 -392
- package/mock-run/orc-budget.md +29 -33
- package/mock-run/orc-cli.md +200 -200
- package/mock-run/orc-explain.md +86 -86
- package/mock-run/orc-extra.md +389 -392
- package/mock-run/orc-fast.md +2 -2
- package/mock-run/orc-fix.md +87 -0
- package/mock-run/orc-pattern.md +112 -112
- package/mock-run/orc-quick.md +146 -146
- package/mock-run/orc.md +4 -4
- package/package.json +1 -1
- package/templates/agents/MODEL-MAPPING.md +176 -173
- package/templates/agents/orc-analyze-mini-sonnet-5-high.md +2 -2
- package/templates/agents/{orc-claude-writer-opus-5-med.md → orc-claude-writer-opus-5-low.md} +4 -4
- package/templates/agents/orc-executor-haiku-4-5.md +1 -1
- package/templates/agents/orc-executor-opus-5-low.md +1 -1
- package/templates/agents/orc-executor-sonnet-4-6-high.md +1 -1
- package/templates/agents/orc-executor-sonnet-4-6-med.md +1 -1
- package/templates/agents/orc-executor-sonnet-5-high.md +2 -2
- package/templates/agents/orc-executor-sonnet-5-low.md +155 -0
- package/templates/agents/orc-executor-sonnet-5-med.md +155 -0
- package/templates/agents/orc-pattern-codifier-sonnet-5-high.md +2 -2
- package/templates/agents/orc-planner-mini-sonnet-5-high.md +2 -2
- package/templates/agents/{orc-recon-sonnet-4-6-med.md → orc-recon-sonnet-5-med.md} +4 -4
- package/templates/agents/orc-retro-sonnet-5-high.md +2 -2
- package/templates/agents/{orc-reviewer-opus-5-med.md → orc-reviewer-opus-5-low.md} +4 -4
- package/templates/agents/{orc-wiki-scanner-opus-5-med.md → orc-wiki-scanner-opus-5-low.md} +3 -3
- package/templates/agents/orc-wiki-scanner-sonnet-5-high.md +2 -2
- package/templates/commands/orc-fast.md +10 -10
- package/templates/commands/orc-fix.md +18 -0
- package/templates/commands/orc-wiki.md +64 -64
- package/templates/hooks/README.md +75 -7
- package/templates/hooks/orc-session-hook.js +78 -3
- package/templates/hooks/orc-statusline-render.js +317 -23
- package/templates/hooks/orc-statusline.js +162 -49
- package/templates/hooks/orc-subagent-line.js +23 -5
- package/templates/hooks/orc-weather-fetch.js +123 -0
- package/templates/skills/_shared/extra-dispatch.md +1331 -1331
- package/templates/skills/_shared/gotchas.md +27 -7
- package/templates/skills/_shared/habits.md +2 -2
- package/templates/skills/_shared/opus5-only.md +137 -137
- package/templates/skills/_shared/phases/review.md +2 -2
- package/templates/skills/_shared/phases/trace-verbs.md +17 -3
- package/templates/skills/_shared/phases/trace.md +136 -136
- package/templates/skills/_shared/review-slice.md +28 -1
- package/templates/skills/_shared/wait.md +1 -0
- package/templates/skills/orc/README.md +2 -2
- package/templates/skills/orc/SKILL.md +227 -227
- package/templates/skills/orc/config.md +25 -27
- package/templates/skills/orc/examples/full-run-mock.md +74 -73
- package/templates/skills/orc/references/effort-and-mode.md +222 -222
- package/templates/skills/orc/references/preflight-report.md +3 -3
- package/templates/skills/orc/references/ultra-mode.md +1 -1
- package/templates/skills/orc/schemas/checkpoint.md +122 -122
- package/templates/skills/orc/subskills/orc-review-verify/SKILL.md +3 -3
- package/templates/skills/orc/subskills/orc-review-verify/core.md +1 -1
- package/templates/skills/orc-analyze/SKILL.md +4 -2
- package/templates/skills/orc-claude/SKILL.md +14 -14
- package/templates/skills/orc-claude/examples/claude-run-mock.md +65 -65
- package/templates/skills/orc-diy/SKILL.md +5 -5
- package/templates/skills/orc-fast/SKILL.md +210 -208
- package/templates/skills/orc-fix/SKILL.md +110 -0
- package/templates/skills/orc-grill/SKILL.md +232 -232
- package/templates/skills/orc-grill/references/grill-doc.md +1 -1
- package/templates/skills/orc-mini/SKILL.md +4 -4
- package/templates/skills/orc-mini/examples/mini-run-mock.md +4 -2
- package/templates/skills/orc-quick/README.md +536 -536
- package/templates/skills/orc-quick/SKILL.md +2 -2
- package/templates/skills/orc-quick/references/context-doc.md +145 -145
- package/templates/skills/orc-quick/references/dispatch-gate.md +220 -219
- package/templates/skills/orc-quick/references/gh-mode.md +4 -2
- package/templates/skills/orc-retro/SKILL.md +1 -1
- package/templates/skills/orc-retro/examples/retro-mock.md +16 -16
- package/templates/skills/orc-wiki/README.md +325 -325
- package/templates/skills/orc-wiki/SKILL.md +195 -195
- package/templates/skills/orc-wiki/examples/wiki-run-mock.md +37 -37
- package/templates/skills/orc-wiki/references/extra.md +2 -2
- package/templates/skills/orc-wiki/references/partial-refresh.md +276 -271
- package/templates/skills/orc-wiki/references/phases/phase-0.md +1 -1
- package/templates/skills/orc-wiki/references/phases/phase-2.md +3 -3
- package/templates/skills/orc-wiki/references/phases/phase-3c.md +3 -2
- package/templates/agents/orc-claude-writer-opus-4-8-high.md +0 -48
- package/templates/agents/orc-wiki-scanner-opus-4-8-high.md +0 -74
package/bin/gotcha.js
CHANGED
|
@@ -63,6 +63,13 @@ const SEVERITIES = ["P0", "P1", "P2", "P3"];
|
|
|
63
63
|
const POLARITIES = ["flag", "suppress"];
|
|
64
64
|
const OUTCOMES = ["addressed", "open", "disputed", "wontfix", "flaky"];
|
|
65
65
|
const AUTHORS = ["human", "bot", "orc"];
|
|
66
|
+
// v2.1.0 §4.2 — a FIX record (`orc fix record`). `introduced_by` is who wrote
|
|
67
|
+
// the bad lines; `cause` is a REQUIRED ledger field already, so it is not reused.
|
|
68
|
+
const INTRODUCED_BY = ["orc", "ai", "human", "unknown"];
|
|
69
|
+
const FIX_VIA = ["orc-fix", "orc-quick", "import"];
|
|
70
|
+
// The fields a fix record keeps when a later import upserts the same row.
|
|
71
|
+
const FIX_FIELDS = ["introduced_by", "fix_run", "via", "miss", "missed_by", "class_by", "run"];
|
|
72
|
+
const SCOPE_CAP = 500; // review-scope.jsonl keeps the last 500 reviews (DE-13)
|
|
66
73
|
// The 8 required fields, in their FIXED order (v1, unchanged) …
|
|
67
74
|
const REQUIRED = ["trigger", "symptom", "cause", "fix", "scope", "origin", "hits", "last_seen"];
|
|
68
75
|
// … then the optional ones, fixed order when present. `cwe` rides beside
|
|
@@ -642,12 +649,93 @@ function suppressLine(v) {
|
|
|
642
649
|
}
|
|
643
650
|
|
|
644
651
|
// ── Quality (acceptance of ORC's own findings) ──────────────────────────────
|
|
645
|
-
|
|
652
|
+
// v2.1.0 — A REVIEW IS ALSO READ FROM THE TRACES. Until 2.1.0 a review counted
|
|
653
|
+
// only when a lane piped an observation with `run`, and no lane did so in
|
|
654
|
+
// practice: the panel said "5 reviews needed — 0 so far" over 23 reviewer
|
|
655
|
+
// returns. A trace run in the window that has a `FINDING-OUTCOME` line OR a
|
|
656
|
+
// hook `RETURN orc-reviewer-*` line (a reviewer the user spawned directly gets a
|
|
657
|
+
// bootstrap trace with that pair) is a review. A clean review (zero findings)
|
|
658
|
+
// counts as a review and adds to nothing else (DE-11). Acceptance and the
|
|
659
|
+
// categories still come from the observations only.
|
|
660
|
+
const TRACE_REVIEW = /^\[[^\]]*\]\s+(?:\S+\s+FINDING-OUTCOME\b|hook\s+RETURN\s+orc-reviewer-)/m;
|
|
661
|
+
function tracedReviewRuns(claudeDir, deps, windowDays, nowMs) {
|
|
662
|
+
if (!deps || typeof deps.listTraces !== "function") return [];
|
|
663
|
+
let runs = [];
|
|
664
|
+
try {
|
|
665
|
+
runs = deps.listTraces(claudeDir).runs;
|
|
666
|
+
} catch (_) {
|
|
667
|
+
return [];
|
|
668
|
+
}
|
|
669
|
+
const out = [];
|
|
670
|
+
for (const r of runs) {
|
|
671
|
+
const t = r.date ? Date.parse(r.date + "T00:00:00Z") : r.mtime;
|
|
672
|
+
if (Number.isFinite(t) && nowMs - t > windowDays * DAY) continue;
|
|
673
|
+
let text = "";
|
|
674
|
+
try {
|
|
675
|
+
text = fs.readFileSync(r.path, "utf8");
|
|
676
|
+
} catch (_) {
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
if (TRACE_REVIEW.test(text)) out.push(r.name.replace(/\.txt$/, ""));
|
|
680
|
+
}
|
|
681
|
+
return out;
|
|
682
|
+
}
|
|
683
|
+
function fixSummary(fixes) {
|
|
684
|
+
const by = (key, vocab) => {
|
|
685
|
+
const out = {};
|
|
686
|
+
for (const v of vocab) out[v] = 0;
|
|
687
|
+
for (const o of fixes) {
|
|
688
|
+
const k = o[key] || "unknown";
|
|
689
|
+
out[k] = (out[k] || 0) + 1;
|
|
690
|
+
}
|
|
691
|
+
return out;
|
|
692
|
+
};
|
|
693
|
+
// `share` is the bar width, so the panel computes no number itself.
|
|
694
|
+
const causes = FIX_CAUSES.map((id) => {
|
|
695
|
+
const n = fixes.filter((o) => fixCause(o) === id).length;
|
|
696
|
+
return { id, n, share: fixes.length ? Math.round((n / fixes.length) * 1000) / 1000 : 0 };
|
|
697
|
+
});
|
|
698
|
+
return { total: fixes.length, by_introduced_by: by("introduced_by", INTRODUCED_BY), by_source: by("source", []), causes };
|
|
699
|
+
}
|
|
700
|
+
function missSummary(fixes) {
|
|
701
|
+
const miss = fixes.filter((o) => o.miss === true);
|
|
702
|
+
const cats = {};
|
|
703
|
+
for (const o of miss) {
|
|
704
|
+
const c = o.category || "uncategorized";
|
|
705
|
+
cats[c] = (cats[c] || 0) + 1;
|
|
706
|
+
}
|
|
707
|
+
return {
|
|
708
|
+
total: miss.length,
|
|
709
|
+
by_category: Object.entries(cats)
|
|
710
|
+
.map(([category, n]) => ({ category, n }))
|
|
711
|
+
.sort((a, b) => b.n - a.n || a.category.localeCompare(b.category)),
|
|
712
|
+
list: miss
|
|
713
|
+
.slice()
|
|
714
|
+
.sort((a, b) => String(b.at).localeCompare(String(a.at)))
|
|
715
|
+
.slice(0, 10)
|
|
716
|
+
.map((o) => ({ obs: o.obs.slice(0, 8), at: o.at, path: o.path, lines: o.lines, source: o.source, rule: o.rule, category: o.category, introduced_by: o.introduced_by || null, missed_by: o.missed_by || null, fix_run: o.fix_run || null })),
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
function quality(obs, entries, windowDays, nowMs, traced) {
|
|
720
|
+
const fixes = fixesIn(obs, windowDays, nowMs);
|
|
646
721
|
const orc = obs.filter((o) => o.author === "orc" && (() => {
|
|
647
722
|
const t = dateMs(o.at);
|
|
648
723
|
return !Number.isFinite(t) || nowMs - t <= windowDays * DAY;
|
|
649
724
|
})());
|
|
650
|
-
|
|
725
|
+
// v2.1.0 §3.4 — the headline counts ORC reviews. A review the project asked for
|
|
726
|
+
// (`reviewer: "<name>"`) is counted in its own line, `by_reviewer`.
|
|
727
|
+
const external = orc.filter((o) => o.reviewer);
|
|
728
|
+
const own = orc.filter((o) => !o.reviewer);
|
|
729
|
+
const observedRuns = [...new Set(own.map((o) => o.run).filter(Boolean))];
|
|
730
|
+
const tracedRuns = [...new Set(traced || [])];
|
|
731
|
+
const runs = [...new Set(observedRuns.concat(tracedRuns))];
|
|
732
|
+
const byReviewer = { orc: runs.length };
|
|
733
|
+
for (const o of external) {
|
|
734
|
+
const k = o.reviewer;
|
|
735
|
+
byReviewer[k] = byReviewer[k] || new Set();
|
|
736
|
+
byReviewer[k].add(o.run || o.ref);
|
|
737
|
+
}
|
|
738
|
+
for (const k of Object.keys(byReviewer)) if (byReviewer[k] instanceof Set) byReviewer[k] = byReviewer[k].size;
|
|
651
739
|
const cats = {};
|
|
652
740
|
for (const o of orc) {
|
|
653
741
|
const c = o.category || "uncategorized";
|
|
@@ -680,6 +768,14 @@ function quality(obs, entries, windowDays, nowMs) {
|
|
|
680
768
|
return {
|
|
681
769
|
window_days: windowDays,
|
|
682
770
|
reviews: runs.length,
|
|
771
|
+
reviews_traced: tracedRuns.length,
|
|
772
|
+
reviews_observed: observedRuns.length,
|
|
773
|
+
by_reviewer: byReviewer,
|
|
774
|
+
project_reviews: Object.entries(byReviewer).filter(([k]) => k !== "orc").reduce((n, [, v]) => n + v, 0),
|
|
775
|
+
// v2.1.0 §4.2 — the fixes made after a review (`orc fix record`). A fix's
|
|
776
|
+
// author is the finder, so none of these is ever counted as a review above.
|
|
777
|
+
fixes: fixSummary(fixes),
|
|
778
|
+
misses: missSummary(fixes),
|
|
683
779
|
floor: QUALITY_FLOOR,
|
|
684
780
|
below_floor: runs.length < QUALITY_FLOOR,
|
|
685
781
|
findings: orc.length,
|
|
@@ -788,6 +884,13 @@ function normalizeObservation(b, now) {
|
|
|
788
884
|
if (!b.ref || typeof b.ref !== "string") return bad("ref", "is required (e.g. \"PR 142 · thread 3\")");
|
|
789
885
|
if (!OUTCOMES.includes(b.outcome)) return bad("outcome", `must be one of: ${OUTCOMES.join(", ")}`);
|
|
790
886
|
if (!AUTHORS.includes(b.author)) return bad("author", `must be one of: ${AUTHORS.join(", ")}`);
|
|
887
|
+
// v2.1.0 — an ORC finding without its run counted as a finding and never as a
|
|
888
|
+
// review. A `ref` that starts with the run id fills `run`; otherwise refuse.
|
|
889
|
+
if (b.author === "orc" && !b.run) {
|
|
890
|
+
const m = typeof b.ref === "string" && /^(run-[a-z0-9][a-z0-9-]*-\d{6}-\d{6})\b/.exec(b.ref);
|
|
891
|
+
if (m) b = Object.assign({}, b, { run: m[1] });
|
|
892
|
+
else return bad("run", "is required when author is orc (the trace name, e.g. run-orc-add-billing-050826-141233)");
|
|
893
|
+
}
|
|
791
894
|
if (b.category !== undefined && b.category !== null && !CATEGORIES.includes(b.category)) return bad("category", `must be one of: ${CATEGORIES.join(", ")}`);
|
|
792
895
|
if (b.severity !== undefined && b.severity !== null && !SEVERITIES.includes(b.severity)) return bad("severity", "must be P0, P1, P2 or P3");
|
|
793
896
|
if (b.cwe !== undefined && b.cwe !== null && !/^CWE-\d+$/.test(b.cwe)) return bad("cwe", "must be CWE-<number>");
|
|
@@ -819,8 +922,141 @@ function normalizeObservation(b, now) {
|
|
|
819
922
|
if (b.reproduced === true) o.reproduced = true;
|
|
820
923
|
if (b.miss === true) o.miss = true;
|
|
821
924
|
if (b.gotcha) o.gotcha = b.gotcha;
|
|
925
|
+
// v2.1.0 — a review the PROJECT asked for (review-slice.md §0 option 2). An
|
|
926
|
+
// OPTIONAL field after the nine, so a 1.9.2 / 2.0.x reader still reads the row.
|
|
927
|
+
if (b.reviewer && typeof b.reviewer === "string" && b.reviewer !== "orc") o.reviewer = oneLine(b.reviewer).slice(0, 60);
|
|
928
|
+
// v2.1.0 §4.2 — a FIX record. OPTIONAL fields after the existing ones. The
|
|
929
|
+
// author is the FINDER (bot or human), never orc: an orc author would make
|
|
930
|
+
// Review Quality count the fix as an ORC review.
|
|
931
|
+
if (b.introduced_by !== undefined && b.introduced_by !== null) {
|
|
932
|
+
if (!INTRODUCED_BY.includes(b.introduced_by)) return bad("introduced_by", `must be one of: ${INTRODUCED_BY.join(", ")}`);
|
|
933
|
+
o.introduced_by = b.introduced_by;
|
|
934
|
+
}
|
|
935
|
+
if (b.via !== undefined && b.via !== null) {
|
|
936
|
+
if (!FIX_VIA.includes(b.via)) return bad("via", `must be one of: ${FIX_VIA.join(", ")}`);
|
|
937
|
+
o.via = b.via;
|
|
938
|
+
}
|
|
939
|
+
if (b.fix_run) o.fix_run = oneLine(b.fix_run).slice(0, 120);
|
|
940
|
+
if (b.missed_by) o.missed_by = oneLine(b.missed_by).slice(0, 120);
|
|
941
|
+
if (b.class_by === "user" || b.class_by === "evidence") o.class_by = b.class_by;
|
|
942
|
+
if ((o.introduced_by || o.via || o.fix_run) && o.author === "orc")
|
|
943
|
+
return bad("author", "of a fix is the FINDER (bot or human) — never orc, or Review Quality counts the fix as an ORC review");
|
|
822
944
|
return { o };
|
|
823
945
|
}
|
|
946
|
+
const isFixRecord = (o) => !!(o && (o.via || o.introduced_by));
|
|
947
|
+
|
|
948
|
+
// ── The reviewed ranges (DE-13) ─────────────────────────────────────────────
|
|
949
|
+
// `.claude/orc/review-scope.jsonl`: one line per review close,
|
|
950
|
+
// `{run, commit, at, files: {path: [[start,end],…]}}`. Class b user data: never
|
|
951
|
+
// pruned by age, never in the install manifest. The last SCOPE_CAP reviews are
|
|
952
|
+
// kept; what is dropped is COUNTED in the first line (`{"meta": …, "dropped": n}`).
|
|
953
|
+
function scopeFile(claudeDir) {
|
|
954
|
+
return path.join(claudeDir, "orc", "review-scope.jsonl");
|
|
955
|
+
}
|
|
956
|
+
function readScopes(claudeDir) {
|
|
957
|
+
const f = scopeFile(claudeDir);
|
|
958
|
+
const out = { rows: [], dropped: 0 };
|
|
959
|
+
if (!fs.existsSync(f)) return out;
|
|
960
|
+
for (const line of fs.readFileSync(f, "utf8").replace(/\r\n/g, "\n").split("\n")) {
|
|
961
|
+
if (!line.trim()) continue;
|
|
962
|
+
let r;
|
|
963
|
+
try {
|
|
964
|
+
r = JSON.parse(line);
|
|
965
|
+
} catch (_) {
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
if (r && r.meta === "review-scope") out.dropped = Number(r.dropped) || 0;
|
|
969
|
+
else if (r && r.run && r.files) out.rows.push(r);
|
|
970
|
+
}
|
|
971
|
+
return out;
|
|
972
|
+
}
|
|
973
|
+
// A body → { row } or { field, err }. Pure.
|
|
974
|
+
function normalizeScope(b, now) {
|
|
975
|
+
const bad = (field, err) => ({ field, err });
|
|
976
|
+
if (!b.run || typeof b.run !== "string") return bad("run", "is required (the trace name of the review's run)");
|
|
977
|
+
if (!b.files || typeof b.files !== "object" || Array.isArray(b.files)) return bad("files", "must be {path: [[start,end], …]}");
|
|
978
|
+
const files = {};
|
|
979
|
+
for (const [p, ranges] of Object.entries(b.files)) {
|
|
980
|
+
if (!Array.isArray(ranges)) return bad("files", `${p}: must be a list of [start,end] pairs`);
|
|
981
|
+
const list = [];
|
|
982
|
+
for (const r of ranges) {
|
|
983
|
+
const pair = Array.isArray(r) ? r : [r, r];
|
|
984
|
+
const s = pair[0];
|
|
985
|
+
const e = pair.length > 1 ? pair[1] : pair[0];
|
|
986
|
+
if (!Number.isInteger(s) || !Number.isInteger(e) || s < 1 || e < s) return bad("files", `${p}: ${JSON.stringify(r)} is not a [start,end] line pair`);
|
|
987
|
+
list.push([s, e]);
|
|
988
|
+
}
|
|
989
|
+
if (list.length) files[posix(p)] = list;
|
|
990
|
+
}
|
|
991
|
+
if (!Object.keys(files).length) return bad("files", "names no reviewed range");
|
|
992
|
+
return { row: { run: oneLine(b.run).slice(0, 120), commit: b.commit ? oneLine(b.commit).slice(0, 40) : null, at: new Date(now).toISOString(), files } };
|
|
993
|
+
}
|
|
994
|
+
function recordScope(claudeDir, row) {
|
|
995
|
+
const f = scopeFile(claudeDir);
|
|
996
|
+
const cur = readScopes(claudeDir);
|
|
997
|
+
const rows = cur.rows.concat(row);
|
|
998
|
+
const over = Math.max(0, rows.length - SCOPE_CAP);
|
|
999
|
+
fs.mkdirSync(path.dirname(f), { recursive: true });
|
|
1000
|
+
if (!over) {
|
|
1001
|
+
fs.appendFileSync(f, JSON.stringify(row) + "\n");
|
|
1002
|
+
return { kept: rows.length, dropped: cur.dropped, dropped_now: 0 };
|
|
1003
|
+
}
|
|
1004
|
+
const dropped = cur.dropped + over;
|
|
1005
|
+
const body = [JSON.stringify({ meta: "review-scope", dropped })].concat(rows.slice(over).map((r) => JSON.stringify(r))).join("\n") + "\n";
|
|
1006
|
+
const tmp = f + ".tmp";
|
|
1007
|
+
fs.writeFileSync(tmp, body);
|
|
1008
|
+
fs.renameSync(tmp, f);
|
|
1009
|
+
return { kept: rows.length - over, dropped, dropped_now: over };
|
|
1010
|
+
}
|
|
1011
|
+
// The latest stored review of an EARLIER run whose ranges overlap the fix's
|
|
1012
|
+
// lines → that scope row, or null. A review older than the commit that wrote
|
|
1013
|
+
// the bad lines could not have seen them, so it is never a miss.
|
|
1014
|
+
function findMiss(scopes, o, introducedMs) {
|
|
1015
|
+
if (!o || !o.path || !Array.isArray(o.lines) || !o.lines.length) return null;
|
|
1016
|
+
const s = o.lines[0];
|
|
1017
|
+
const e = o.lines.length > 1 ? o.lines[1] : o.lines[0];
|
|
1018
|
+
let best = null;
|
|
1019
|
+
for (const r of scopes) {
|
|
1020
|
+
if (!r.run || (o.fix_run && r.run === o.fix_run)) continue;
|
|
1021
|
+
const ranges = r.files && r.files[o.path];
|
|
1022
|
+
if (!Array.isArray(ranges) || !ranges.some((x) => x[0] <= e && s <= x[1])) continue;
|
|
1023
|
+
const at = Date.parse(r.at);
|
|
1024
|
+
if (Number.isFinite(introducedMs) && Number.isFinite(at) && at < introducedMs) continue;
|
|
1025
|
+
if (!best || at > Date.parse(best.at)) best = r;
|
|
1026
|
+
}
|
|
1027
|
+
return best;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// The CLI computes `miss` for a fix record (§4.2 item 4) from the stored scopes.
|
|
1031
|
+
function applyMiss(claudeDir, o, introducedMs) {
|
|
1032
|
+
const hit = findMiss(readScopes(claudeDir).rows, o, introducedMs);
|
|
1033
|
+
return hit ? Object.assign({}, o, { miss: true, missed_by: hit.run }) : o;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// ── The fix records in a window: the card line and the quality block ────────
|
|
1037
|
+
function fixesIn(obs, windowDays, nowMs) {
|
|
1038
|
+
return obs.filter((o) => {
|
|
1039
|
+
if (!isFixRecord(o)) return false;
|
|
1040
|
+
const t = dateMs(o.at);
|
|
1041
|
+
return !Number.isFinite(t) || nowMs - t <= windowDays * DAY;
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
const FIX_LABEL = { sonar: "Sonar", ci: "CI", defect: "defect", pr: "PR review", review: "review", sarif: "SARIF" };
|
|
1045
|
+
function fixLine(o, n) {
|
|
1046
|
+
const what = o.rule ? o.rule.replace(/^sonar:/, "") : o.sig ? o.sig.slice(0, 60) : "a fix";
|
|
1047
|
+
const after =
|
|
1048
|
+
o.introduced_by === "orc" ? ` after ORC run ${o.run || "(run unknown)"}` : o.introduced_by === "ai" ? " after AI-written code" : "";
|
|
1049
|
+
return `fix: ${o.path} — ${FIX_LABEL[o.source] || o.source} ${what}${o.category ? ` (${o.category})` : ""}${after}${o.miss ? " · missed by review" : ""}${n > 1 ? ` (+${n - 1} more)` : ""}`;
|
|
1050
|
+
}
|
|
1051
|
+
// The cause buckets the panel draws, in its order. Computed HERE, so the panel
|
|
1052
|
+
// computes no number the CLI could give.
|
|
1053
|
+
const FIX_CAUSES = ["sonar", "orc", "ai", "other"];
|
|
1054
|
+
function fixCause(o) {
|
|
1055
|
+
if (o.source === "sonar") return "sonar";
|
|
1056
|
+
if (o.introduced_by === "orc") return "orc";
|
|
1057
|
+
if (o.introduced_by === "ai") return "ai";
|
|
1058
|
+
return "other";
|
|
1059
|
+
}
|
|
824
1060
|
|
|
825
1061
|
// Append each normalized observation, bump what an active entry already covers,
|
|
826
1062
|
// then run promotion ONCE. `skipUnchanged`: an importer re-reading the same
|
|
@@ -831,8 +1067,15 @@ function recordObservations(store, led, list, now, opts) {
|
|
|
831
1067
|
const changes = { bumped: [], promoted: [], helpful: [], harmful: [] };
|
|
832
1068
|
const recorded = [];
|
|
833
1069
|
let unchanged = 0;
|
|
834
|
-
for (
|
|
1070
|
+
for (let o of list) {
|
|
835
1071
|
const prev = known.get(o.obs) || null;
|
|
1072
|
+
// v2.1.0 §4.2 — an import that upserts a FIX record's row (the same id,
|
|
1073
|
+
// `sha1("sonar|sonar <key>")`) adds its outcome and keeps the fix fields.
|
|
1074
|
+
if (prev && isFixRecord(prev)) {
|
|
1075
|
+
const keep = {};
|
|
1076
|
+
for (const k of FIX_FIELDS) if (prev[k] !== undefined && prev[k] !== null && (o[k] === undefined || o[k] === null)) keep[k] = prev[k];
|
|
1077
|
+
if (Object.keys(keep).length) o = Object.assign({}, o, keep);
|
|
1078
|
+
}
|
|
836
1079
|
if (skipUnchanged && prev && prev.outcome === o.outcome) {
|
|
837
1080
|
unchanged++;
|
|
838
1081
|
continue;
|
|
@@ -878,7 +1121,8 @@ const USAGE =
|
|
|
878
1121
|
" orc gotcha match --files <csv> the executor block (exit 0 · 1 no match · 4 off)\n" +
|
|
879
1122
|
" orc gotcha card --files <csv> [--lane <l>] [--full] the reviewer card (exit 0 · 1 no match)\n" +
|
|
880
1123
|
" orc gotcha filter --findings <file|-> drop suppressed / folded / noisy advice; never a P0 or P1\n" +
|
|
881
|
-
" orc gotcha observe <file|-> append one observation, then run promotion (exit 0 · 2 malformed)
|
|
1124
|
+
" orc gotcha observe <file|-> append one observation, then run promotion (exit 0 · 2 malformed);\n" +
|
|
1125
|
+
" a {kind: \"review-scope\"} body stores the ranges a review read\n" +
|
|
882
1126
|
" orc gotcha list --candidates computed candidates and proposed suppressions (exit 0 · 1 none)\n" +
|
|
883
1127
|
" orc gotcha accept <C-id> a person promotes a candidate (exit 0 · 2 unknown)\n" +
|
|
884
1128
|
" orc gotcha quality [--window <days>] acceptance per category (exit 0 · 1 below the 5-review floor)\n" +
|
|
@@ -1002,8 +1246,19 @@ function gotchaCmd(deps, sub) {
|
|
|
1002
1246
|
const cands = candidates(led.entries, obs, now).filter((c) => c.watch && files.some((f) => inScope(c.scope, f)));
|
|
1003
1247
|
for (const c of cands)
|
|
1004
1248
|
rows.push({ id: c.id, type: "watch", text: watchLine(c), rank: (SEV_W[c.severity] || 2) * Math.log(1 + c.addressed) * specificity(c.scope), first: false });
|
|
1249
|
+
// v2.1.0 §4.2 item 5 — ONE `fix` line per changed file that has a fix
|
|
1250
|
+
// record in the window: the newest, a miss first. Inside the budget; a line
|
|
1251
|
+
// that does not fit is counted like any other.
|
|
1252
|
+
const fixes = fixesIn(obs, WINDOW_DAYS, now);
|
|
1253
|
+
for (const f of files) {
|
|
1254
|
+
const mine = fixes.filter((o) => o.path === f);
|
|
1255
|
+
if (!mine.length) continue;
|
|
1256
|
+
mine.sort((a, b) => (b.miss ? 1 : 0) - (a.miss ? 1 : 0) || String(b.at).localeCompare(String(a.at)));
|
|
1257
|
+
rows.push({ id: "fix:" + f, type: "fix", text: fixLine(mine[0], mine.length), rank: mine.length, first: !!mine[0].miss, miss: !!mine[0].miss });
|
|
1258
|
+
}
|
|
1005
1259
|
const byRank = (a, b) => (b.first ? 1 : 0) - (a.first ? 1 : 0) || b.rank - a.rank || a.id.localeCompare(b.id);
|
|
1006
1260
|
const ordered = rows.filter((r) => r.type === "flag").sort(byRank)
|
|
1261
|
+
.concat(rows.filter((r) => r.type === "fix").sort(byRank))
|
|
1007
1262
|
.concat(rows.filter((r) => r.type === "watch").sort(byRank))
|
|
1008
1263
|
.concat(rows.filter((r) => r.type === "suppress").sort(byRank));
|
|
1009
1264
|
const matched = ordered.length;
|
|
@@ -1021,7 +1276,7 @@ function gotchaCmd(deps, sub) {
|
|
|
1021
1276
|
r.dropped = "watch cap";
|
|
1022
1277
|
continue;
|
|
1023
1278
|
}
|
|
1024
|
-
if (r.type
|
|
1279
|
+
if ((r.type === "flag" || r.type === "watch") && nFlag + nWatch >= CARD_FLAG_CAP) {
|
|
1025
1280
|
r.dropped = "line cap";
|
|
1026
1281
|
continue;
|
|
1027
1282
|
}
|
|
@@ -1037,7 +1292,7 @@ function gotchaCmd(deps, sub) {
|
|
|
1037
1292
|
kept.push(r);
|
|
1038
1293
|
if (r.type === "flag") nFlag++;
|
|
1039
1294
|
else if (r.type === "watch") nWatch++;
|
|
1040
|
-
else nSup++;
|
|
1295
|
+
else if (r.type === "suppress") nSup++;
|
|
1041
1296
|
}
|
|
1042
1297
|
const dropped = matched - kept.length;
|
|
1043
1298
|
const text = [headerFor(matched, dropped)].concat(kept.map((k) => k.text)).join("\n");
|
|
@@ -1124,11 +1379,23 @@ function gotchaCmd(deps, sub) {
|
|
|
1124
1379
|
if (sub === "observe") {
|
|
1125
1380
|
const inp = readInput(pos[2] || (args.includes("-") ? "-" : undefined));
|
|
1126
1381
|
if (inp.err) return fail(2, "malformed", `gotcha observe: the input ${inp.err}`, { field: "body" });
|
|
1382
|
+
// v2.1.0 DE-13 — a review close stores the ranges it read. Not an
|
|
1383
|
+
// observation: it goes to review-scope.jsonl, which `orc fix record` reads.
|
|
1384
|
+
if (inp.value && inp.value.kind === "review-scope") {
|
|
1385
|
+
const s = normalizeScope(inp.value, now);
|
|
1386
|
+
if (s.err) return fail(2, "malformed", `gotcha observe: review-scope field \`${s.field}\` ${s.err}`, { field: s.field });
|
|
1387
|
+
const w = recordScope(claudeDir, s.row);
|
|
1388
|
+
return out({ ok: true, kind: "review-scope", run: s.row.run, files: Object.keys(s.row.files).length, kept: w.kept, dropped: w.dropped, cap: SCOPE_CAP, file: scopeFile(claudeDir) }, 0, () =>
|
|
1389
|
+
console.log(`✓ review scope stored for ${s.row.run} (${Object.keys(s.row.files).length} file(s) · ${w.kept} of the last ${SCOPE_CAP} reviews kept${w.dropped ? ` · ${w.dropped} older dropped` : ""})`)
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1127
1392
|
const n = normalizeObservation(inp.value, now);
|
|
1128
1393
|
if (n.err) return fail(2, "malformed", `gotcha observe: field \`${n.field}\` ${n.err}`, { field: n.field });
|
|
1129
|
-
|
|
1394
|
+
let o = n.o;
|
|
1395
|
+
if (isFixRecord(o) && !o.miss) o = applyMiss(claudeDir, o, NaN);
|
|
1130
1396
|
const { recorded, changes, all } = recordObservations(store, led, [o], now);
|
|
1131
1397
|
const prev = recorded[0].prev;
|
|
1398
|
+
o = recorded[0].o;
|
|
1132
1399
|
const after = candidates(led.entries, all, now);
|
|
1133
1400
|
const mine = after.find((c) => c.obs.includes(o.obs));
|
|
1134
1401
|
const payload = {
|
|
@@ -1195,7 +1462,7 @@ function gotchaCmd(deps, sub) {
|
|
|
1195
1462
|
if (sub === "quality") {
|
|
1196
1463
|
const w = Number(valueOf("--window"));
|
|
1197
1464
|
const windowDays = Number.isInteger(w) && w > 0 ? w : WINDOW_DAYS;
|
|
1198
|
-
const q = quality(obs, led.entries, windowDays, now);
|
|
1465
|
+
const q = quality(obs, led.entries, windowDays, now, tracedReviewRuns(claudeDir, deps, windowDays, now));
|
|
1199
1466
|
return out(Object.assign({ ok: true }, q), q.below_floor ? 1 : 0, () => {
|
|
1200
1467
|
if (q.below_floor) console.log(`gotcha quality: ${q.reviews} review(s) in ${windowDays} days — below the ${QUALITY_FLOOR}-review floor, no answer yet`);
|
|
1201
1468
|
for (const c of q.categories) console.log(` ${c.category.padEnd(28)} ${c.findings} findings · acceptance ${c.acceptance === null ? "—" : Math.round(c.acceptance * 100) + " %"}${c.noisy ? " · noisy" : ""}`);
|
|
@@ -1281,6 +1548,19 @@ module.exports = {
|
|
|
1281
1548
|
makeStore,
|
|
1282
1549
|
normalizeObservation,
|
|
1283
1550
|
recordObservations,
|
|
1551
|
+
// v2.1.0 W6 — `orc fix` (bin/fix.js) records through these.
|
|
1552
|
+
INTRODUCED_BY,
|
|
1553
|
+
FIX_CAUSES,
|
|
1554
|
+
SCOPE_CAP,
|
|
1555
|
+
isFixRecord,
|
|
1556
|
+
readScopes,
|
|
1557
|
+
normalizeScope,
|
|
1558
|
+
recordScope,
|
|
1559
|
+
findMiss,
|
|
1560
|
+
applyMiss,
|
|
1561
|
+
fixesIn,
|
|
1562
|
+
fixLine,
|
|
1563
|
+
dateMs,
|
|
1284
1564
|
redact,
|
|
1285
1565
|
sha1,
|
|
1286
1566
|
};
|