@gobing-ai/spur 0.3.62 → 0.3.63
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/.claude-plugin/marketplace.json +1 -1
- package/config/corpus-baseline.json +200 -8
- package/config/workflows/history-anatomy.yaml +40 -11
- package/package.json +9 -9
- package/plugins/sp/README.md +2 -2
- package/plugins/sp/commands/dev-find-issue.md +4 -4
- package/plugins/sp/commands/dev-idea.md +3 -3
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/references/environment-lens.md +66 -0
- package/plugins/sp/scripts/history-anatomy-cache.mjs +162 -13
- package/plugins/sp/scripts/history-anatomy-cache.ts +209 -13
- package/plugins/sp/skills/dogfood-testing/references/report-template.md +30 -0
- package/plugins/sp/skills/history-anatomy/SKILL.md +3 -3
- package/plugins/sp/skills/history-anatomy/references/operations.md +21 -8
- package/plugins/sp/skills/history-anatomy/references/report-contract.md +55 -5
- package/plugins/sp/skills/spur-cli/references/agent.md +15 -6
- package/plugins/sp/skills/spur-cli/references/message.md +4 -2
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +6 -4
- package/spur.js +882 -325
- package/web/_astro/{BoardApp.rkWGVwwK.js → BoardApp.CKolAjUz.js} +56 -56
- package/web/_astro/BoardApp.DXD--ybM.js +1 -0
- package/web/_astro/{TaskDetail.B7ODt_bA.js → TaskDetail.Bre7G4gC.js} +1 -1
- package/web/_astro/{arc.C5QRz6AQ.js → arc.7luwOGiC.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.CHptn8gZ.js → architectureDiagram-3BPJPVTR.F6KaHXp-.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.b1nEDkDw.js → blockDiagram-GPEHLZMM.CCGHeRVi.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.BWj8S_Qq.js → c4Diagram-AAUBKEIU.CpqewGmd.js} +1 -1
- package/web/_astro/channel.DxfOFf1l.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.q0BBuE1n.js → chunk-2J33WTMH.CB9vKa5F.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.DP2KDkAU.js → chunk-4BX2VUAB.ifGXoUA3.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.CkCId72p.js → chunk-55IACEB6.VIaRo7l8.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.oHWzxMot.js → chunk-727SXJPM.DzPE41OS.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.DkkgxfMP.js → chunk-AQP2D5EJ.UF2QRXYF.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.lmFJDvRK.js → chunk-FMBD7UC4.D2zXKa1R.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.52jt0eHK.js → chunk-ND2GUHAM.Cb9bDyvx.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.Cg8KEFoD.js → chunk-QZHKN3VN.nUKFBiLD.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.BuOUhxcD.js → classDiagram-4FO5ZUOK.DZg9K9mO.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.BuOUhxcD.js → classDiagram-v2-Q7XG4LA2.DZg9K9mO.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.BRUU8E_0.js → cose-bilkent-S5V4N54A.D9STo90d.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.GDbfFYVV.js → dagre-BM42HDAG.D3IbwhHz.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.CvIDBeJF.js → diagram-2AECGRRQ.BWTDxBe9.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.DMgJjWOX.js → diagram-5GNKFQAL.XnXlonHG.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.njjl-0AP.js → diagram-KO2AKTUF.CywOngCO.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.CcCqgP8M.js → diagram-LMA3HP47.DY1D21Iu.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.Btqd-YVE.js → diagram-OG6HWLK6.DQxb59KA.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.BbhML_Xo.js → erDiagram-TEJ5UH35.CF2U-pQZ.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.CkvsIgY8.js → flowDiagram-I6XJVG4X.BJK4M3in.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.m8IWD_wW.js → ganttDiagram-6RSMTGT7.BSniMzdB.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.5z87HXO-.js → gitGraphDiagram-PVQCEYII.Dxg-yRov.js} +1 -1
- package/web/_astro/index.BVXdIsZV.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.Bvyvsd7Q.js → infoDiagram-5YYISTIA.BY4CgO_n.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.DdWVYO87.js → ishikawaDiagram-YF4QCWOH.BmWDZtwF.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.C3kOlYH1.js → journeyDiagram-JHISSGLW.CpB1YWDP.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.CfKTUHVC.js → kanban-definition-UN3LZRKU.k-fukQX9.js} +1 -1
- package/web/_astro/{linear.CMDHnzgX.js → linear.BNNCobvI.js} +1 -1
- package/web/_astro/{mermaid.core.DN7-WrsP.js → mermaid.core.DnpzzuPU.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.DcQxLvqG.js → mindmap-definition-RKZ34NQL.D9NnlLBu.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.Nu-Inbw6.js → pieDiagram-4H26LBE5.CKhoMiyC.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.Dck2EChs.js → quadrantDiagram-W4KKPZXB.BkAygRlm.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.C--E5XuW.js → requirementDiagram-4Y6WPE33.CX8ibmwc.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.B_0iRNna.js → sankeyDiagram-5OEKKPKP.Dvurpa0Y.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.PKuKr9mk.js → sequenceDiagram-3UESZ5HK.veO8c2tk.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.ClhGHb-O.js → stateDiagram-AJRCARHV.DpMr4CO3.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.Bv6BBnvg.js → stateDiagram-v2-BHNVJYJU.CKjso86_.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.BZm2ZvAH.js → timeline-definition-PNZ67QCA.DayoPp_2.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.Dt6yZXry.js → vennDiagram-CIIHVFJN.XNHf04O9.js} +1 -1
- package/web/_astro/{wardley-L42UT6IY.rUs-E00M.js → wardley-L42UT6IY.CpM_031g.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.BcWgv4cA.js → wardleyDiagram-YWT4CUSO.lIAjSkZJ.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.DH-M-cX9.js → xychartDiagram-2RQKCTM6.D8_2K6U1.js} +1 -1
- package/web/index.html +2 -2
- package/web/_astro/BoardApp.Z9jMwI9G.js +0 -1
- package/web/_astro/channel.BJtn6CGV.js +0 -1
- package/web/_astro/index.nWve6EHS.css +0 -1
|
@@ -3,6 +3,7 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
4
|
|
|
5
5
|
// plugins/sp/scripts/history-anatomy-cache.ts
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
6
7
|
import { createHash as createHash2 } from "node:crypto";
|
|
7
8
|
import {
|
|
8
9
|
closeSync,
|
|
@@ -63,7 +64,7 @@ function semanticArtifactDigest(artifactJson) {
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
// plugins/sp/scripts/history-anatomy-cache.ts
|
|
66
|
-
var
|
|
67
|
+
var REPORT_SECTIONS = [
|
|
67
68
|
"Scope and provenance",
|
|
68
69
|
"Executive summary",
|
|
69
70
|
"Baseline comparison",
|
|
@@ -73,6 +74,7 @@ var ELEVEN_SECTIONS = [
|
|
|
73
74
|
"Remediation options",
|
|
74
75
|
"Performance analysis",
|
|
75
76
|
"Workflow and process improvements",
|
|
77
|
+
"Report-only advisories",
|
|
76
78
|
"Positive patterns",
|
|
77
79
|
"Evidence ledger"
|
|
78
80
|
];
|
|
@@ -85,7 +87,10 @@ var FINDING_FIELDS = [
|
|
|
85
87
|
"inference",
|
|
86
88
|
"confidence",
|
|
87
89
|
"contradictions",
|
|
88
|
-
"evidenceAnchor"
|
|
90
|
+
"evidenceAnchor",
|
|
91
|
+
"severity",
|
|
92
|
+
"reproCommand",
|
|
93
|
+
"ownerSurface"
|
|
89
94
|
];
|
|
90
95
|
function parseScalar(raw) {
|
|
91
96
|
const t = raw.trim();
|
|
@@ -249,12 +254,21 @@ function decideCache(cached, current, opts) {
|
|
|
249
254
|
function escapeRe(s) {
|
|
250
255
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
251
256
|
}
|
|
257
|
+
var FINDING_CATEGORIES = [
|
|
258
|
+
"reliability",
|
|
259
|
+
"repetition",
|
|
260
|
+
"workflow",
|
|
261
|
+
"performance",
|
|
262
|
+
"coverage",
|
|
263
|
+
"telemetry",
|
|
264
|
+
"positive"
|
|
265
|
+
];
|
|
252
266
|
function checkReportStructure(reportMarkdown) {
|
|
253
267
|
const problems = [];
|
|
254
268
|
if (/TODO|PLACEHOLDER|FIXME|^\|\s*\|/im.test(reportMarkdown))
|
|
255
269
|
problems.push("placeholder-or-todo-present");
|
|
256
270
|
let lastIdx = -1;
|
|
257
|
-
for (const section of
|
|
271
|
+
for (const section of REPORT_SECTIONS) {
|
|
258
272
|
const re = new RegExp(`^#{2,3}\\s+${escapeRe(section)}\\s*$`, "m");
|
|
259
273
|
const m = reportMarkdown.match(re);
|
|
260
274
|
if (m === null || (m.index ?? -1) <= lastIdx) {
|
|
@@ -263,11 +277,45 @@ function checkReportStructure(reportMarkdown) {
|
|
|
263
277
|
lastIdx = m.index;
|
|
264
278
|
}
|
|
265
279
|
}
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
280
|
+
const findingsIdx = reportMarkdown.search(/^##\s+Findings\s*$/im);
|
|
281
|
+
if (findingsIdx !== -1) {
|
|
282
|
+
const tail = reportMarkdown.slice(findingsIdx);
|
|
283
|
+
const nextSection = tail.slice(1).search(/^##\s+/im);
|
|
284
|
+
const findingsBody = nextSection === -1 ? tail : tail.slice(0, nextSection + 1);
|
|
285
|
+
const catAlt = FINDING_CATEGORIES.join("|");
|
|
286
|
+
const knownRows = findingsBody.match(new RegExp(`^\\|\\s*(?:${catAlt}):[^|]+`, "gm")) ?? [];
|
|
287
|
+
const invalidRows = findingsBody.match(/^\|\s*([^|:\s][^|:]*):[^|]+/gm) ?? [];
|
|
288
|
+
for (const row of [...new Set([...knownRows, ...invalidRows])]) {
|
|
289
|
+
const segMatch = row.match(/^\|\s*([^:|]+):/);
|
|
290
|
+
if (segMatch && !FINDING_CATEGORIES.includes(segMatch[1].trim())) {
|
|
291
|
+
problems.push(`finding-invalid-key-category:${segMatch[1].trim()}`);
|
|
292
|
+
}
|
|
293
|
+
const rowLower = row.toLowerCase();
|
|
294
|
+
for (const field of FINDING_FIELDS) {
|
|
295
|
+
if (!rowLower.includes(field.toLowerCase()))
|
|
296
|
+
problems.push(`finding-missing-field:${field}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const blocks = findingsBody.split(/^###\s+/m).slice(1);
|
|
300
|
+
for (const block of blocks) {
|
|
301
|
+
if (!block.includes("`key`") && !/\|\s*key\s*:/.test(block))
|
|
302
|
+
continue;
|
|
303
|
+
for (const field of FINDING_FIELDS) {
|
|
304
|
+
if (!block.includes(field))
|
|
305
|
+
problems.push(`finding-missing-field:${field}`);
|
|
306
|
+
}
|
|
307
|
+
if (!/(^|[\s`])P[123]([\s`.]|$)/.test(block) && !block.includes("symbolic-severity")) {
|
|
308
|
+
problems.push("finding-invalid-severity");
|
|
309
|
+
}
|
|
310
|
+
const catValue = block.match(/`category`\s*[:=]\s*`?([^`\n]+?)`?\s*(?:\n|$)/)?.[1];
|
|
311
|
+
if (catValue && !FINDING_CATEGORIES.includes(catValue.trim())) {
|
|
312
|
+
problems.push(`finding-invalid-category:${catValue.trim()}`);
|
|
313
|
+
}
|
|
314
|
+
const keyValue = block.match(/`key`\s*[:=]\s*`?([^`\n]+?)`?\s*(?:\n|$)/)?.[1] ?? "";
|
|
315
|
+
const firstSegment = keyValue.split(":")[0]?.trim() ?? "";
|
|
316
|
+
if (keyValue !== "" && !FINDING_CATEGORIES.includes(firstSegment)) {
|
|
317
|
+
problems.push(`finding-invalid-key-category:${firstSegment}`);
|
|
318
|
+
}
|
|
271
319
|
}
|
|
272
320
|
}
|
|
273
321
|
const ledgerIdx = reportMarkdown.search(/^#{2,3}\s+Evidence\s+ledger/im);
|
|
@@ -319,17 +367,68 @@ function localDay(tz, at = new Date) {
|
|
|
319
367
|
return at.toISOString().slice(0, 10);
|
|
320
368
|
}
|
|
321
369
|
}
|
|
370
|
+
function tzOffsetMs(tz, at) {
|
|
371
|
+
const parts = Object.fromEntries(new Intl.DateTimeFormat("en-US", {
|
|
372
|
+
timeZone: tz,
|
|
373
|
+
hour12: false,
|
|
374
|
+
year: "numeric",
|
|
375
|
+
month: "2-digit",
|
|
376
|
+
day: "2-digit",
|
|
377
|
+
hour: "2-digit",
|
|
378
|
+
minute: "2-digit",
|
|
379
|
+
second: "2-digit"
|
|
380
|
+
}).formatToParts(at).filter((p) => p.type !== "literal").map((p) => [p.type, p.value]));
|
|
381
|
+
const asUtc = Date.UTC(Number(parts.year), Number(parts.month) - 1, Number(parts.day), parts.hour === "24" ? 0 : Number(parts.hour), Number(parts.minute), Number(parts.second));
|
|
382
|
+
const atSec = Math.floor(at.getTime() / 1000) * 1000;
|
|
383
|
+
return asUtc - atSec;
|
|
384
|
+
}
|
|
385
|
+
function zonedDayStart(tz, ymd) {
|
|
386
|
+
const [y, m, d] = ymd.split("-").map(Number);
|
|
387
|
+
const utcMidnight = Date.UTC(y ?? 1970, (m ?? 1) - 1, d ?? 1);
|
|
388
|
+
const guess = new Date(utcMidnight - tzOffsetMs(tz, new Date(utcMidnight)));
|
|
389
|
+
return new Date(utcMidnight - tzOffsetMs(tz, guess));
|
|
390
|
+
}
|
|
391
|
+
function formatZonedIso(tz, at) {
|
|
392
|
+
const off = tzOffsetMs(tz, at);
|
|
393
|
+
const abs = Math.abs(off);
|
|
394
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
395
|
+
const offStr = `${off < 0 ? "-" : "+"}${pad(Math.floor(abs / 3600000))}:${pad(Math.floor(abs % 3600000 / 60000))}`;
|
|
396
|
+
return `${new Date(at.getTime() + off).toISOString().slice(0, 23)}${offStr}`;
|
|
397
|
+
}
|
|
398
|
+
function dayBounds(tz, ymd) {
|
|
399
|
+
const start = zonedDayStart(tz, ymd);
|
|
400
|
+
const nextYmd = localDay(tz, new Date(start.getTime() + 30 * 3600000));
|
|
401
|
+
return {
|
|
402
|
+
since: formatZonedIso(tz, start),
|
|
403
|
+
until: formatZonedIso(tz, new Date(zonedDayStart(tz, nextYmd).getTime() - 1))
|
|
404
|
+
};
|
|
405
|
+
}
|
|
322
406
|
function resolvePaths(opts) {
|
|
323
407
|
const pluginRoot = opts.helper.replace(/\/scripts\/[^/]+$/, "");
|
|
324
408
|
const skill = `${pluginRoot}/skills/history-anatomy`;
|
|
325
|
-
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone ?? "UTC";
|
|
409
|
+
const tz = opts.tz ?? Intl.DateTimeFormat().resolvedOptions().timeZone ?? "UTC";
|
|
326
410
|
const date = opts.date !== undefined && opts.date !== "" ? opts.date : localDay(tz, opts.now ?? new Date);
|
|
327
411
|
const target = opts.output !== undefined && opts.output !== "" ? opts.output : `${opts.reportDir}/${date}-history-anatomy.md`;
|
|
328
|
-
|
|
412
|
+
const adHoc = opts.mode === "ad-hoc" && !!opts.since && !!opts.until;
|
|
413
|
+
let env = `HA_HELPER=${opts.helper}
|
|
329
414
|
HA_SKILL=${skill}
|
|
330
415
|
HA_TARGET=${target}
|
|
331
416
|
HA_DATE=${date}
|
|
332
417
|
`;
|
|
418
|
+
if (adHoc) {
|
|
419
|
+
return `${env}HA_SINCE=${opts.since}
|
|
420
|
+
HA_UNTIL=${opts.until}
|
|
421
|
+
`;
|
|
422
|
+
}
|
|
423
|
+
const current = dayBounds(tz, date);
|
|
424
|
+
const baseline = dayBounds(tz, localDay(tz, new Date(zonedDayStart(tz, date).getTime() - 12 * 3600000)));
|
|
425
|
+
env += `HA_SINCE=${current.since}
|
|
426
|
+
HA_UNTIL=${current.until}
|
|
427
|
+
`;
|
|
428
|
+
env += `HA_BASELINE_SINCE=${baseline.since}
|
|
429
|
+
HA_BASELINE_UNTIL=${baseline.until}
|
|
430
|
+
`;
|
|
431
|
+
return env;
|
|
333
432
|
}
|
|
334
433
|
function buildProvenance(opts) {
|
|
335
434
|
let raw;
|
|
@@ -486,7 +585,15 @@ function publishAtomically(candidatePath, targetPath) {
|
|
|
486
585
|
throw err;
|
|
487
586
|
}
|
|
488
587
|
}
|
|
489
|
-
|
|
588
|
+
function porcelainPaths(text) {
|
|
589
|
+
return new Set(text.split(`
|
|
590
|
+
`).map((line) => line.replace(/^\S+\s+/, "").trim()).filter((line) => line.length > 0));
|
|
591
|
+
}
|
|
592
|
+
function diffPorcelain(before, now, expects) {
|
|
593
|
+
const beforePaths = porcelainPaths(before);
|
|
594
|
+
return [...porcelainPaths(now)].filter((p) => !beforePaths.has(p) && !expects.has(p)).sort();
|
|
595
|
+
}
|
|
596
|
+
var VALID_COMMANDS = "digest, check, paths, assert-clean, probe, stamp, refresh, publish";
|
|
490
597
|
var PROBE_USAGE = "<script> probe --artifact <a.json> --target <report.md> [--baseline <b.json>] [--mode daily|ad-hoc] " + "[--date <YYYY-MM-DD>] [--recompute true] [--out <prov.json>] [--skill-dir <d>] [--contract <f>] [--workflow <f>]";
|
|
491
598
|
function parseFlags(args) {
|
|
492
599
|
const out = {};
|
|
@@ -543,13 +650,50 @@ ${result.problems.map((p) => `- ${p}
|
|
|
543
650
|
publishAtomically(a, b);
|
|
544
651
|
return { exitCode: 0, stdout: "", stderr: "" };
|
|
545
652
|
}
|
|
653
|
+
case "assert-clean": {
|
|
654
|
+
const f = parseFlags(argv.slice(1));
|
|
655
|
+
if (f.baseline === undefined) {
|
|
656
|
+
return {
|
|
657
|
+
exitCode: 1,
|
|
658
|
+
stdout: "",
|
|
659
|
+
stderr: `usage: <script> assert-clean --baseline <porcelain.txt> [--expect <path>]...
|
|
660
|
+
`
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
const expects = new Set;
|
|
664
|
+
for (const arg of argv.slice(1)) {
|
|
665
|
+
if (arg.startsWith("--expect="))
|
|
666
|
+
expects.add(arg.slice("--expect=".length));
|
|
667
|
+
}
|
|
668
|
+
let now;
|
|
669
|
+
try {
|
|
670
|
+
now = spawnSync("git", ["status", "--porcelain"], {
|
|
671
|
+
encoding: "utf8",
|
|
672
|
+
...f.cwd !== undefined ? { cwd: f.cwd } : {}
|
|
673
|
+
}).stdout ?? "";
|
|
674
|
+
} catch {
|
|
675
|
+
return { exitCode: 0, stdout: "", stderr: `assert-clean: git unavailable; skipped
|
|
676
|
+
` };
|
|
677
|
+
}
|
|
678
|
+
const undeclared = diffPorcelain(readFileSync(f.baseline, "utf8"), now, expects);
|
|
679
|
+
if (undeclared.length > 0) {
|
|
680
|
+
return {
|
|
681
|
+
exitCode: 1,
|
|
682
|
+
stdout: "",
|
|
683
|
+
stderr: undeclared.map((p) => `undeclared write: ${p}
|
|
684
|
+
`).join("")
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
return { exitCode: 0, stdout: `clean
|
|
688
|
+
`, stderr: "" };
|
|
689
|
+
}
|
|
546
690
|
case "paths": {
|
|
547
691
|
const f = parseFlags(argv.slice(1));
|
|
548
692
|
if (f.helper === undefined || f.out === undefined) {
|
|
549
693
|
return {
|
|
550
694
|
exitCode: 1,
|
|
551
695
|
stdout: "",
|
|
552
|
-
stderr: `usage: <script> paths --helper <p> --out <env> [--report-dir <d>] [--date <d>] [--output <p>]
|
|
696
|
+
stderr: `usage: <script> paths --helper <p> --out <env> [--report-dir <d>] [--date <d>] [--output <p>] [--mode <m>] [--since <s>] [--until <u>]
|
|
553
697
|
`
|
|
554
698
|
};
|
|
555
699
|
}
|
|
@@ -557,7 +701,10 @@ ${result.problems.map((p) => `- ${p}
|
|
|
557
701
|
helper: f.helper,
|
|
558
702
|
reportDir: f["report-dir"] ?? "docs/report",
|
|
559
703
|
date: f.date,
|
|
560
|
-
output: f.output
|
|
704
|
+
output: f.output,
|
|
705
|
+
mode: f.mode,
|
|
706
|
+
since: f.since,
|
|
707
|
+
until: f.until
|
|
561
708
|
}));
|
|
562
709
|
return { exitCode: 0, stdout: "", stderr: "" };
|
|
563
710
|
}
|
|
@@ -659,9 +806,11 @@ export {
|
|
|
659
806
|
refreshReport,
|
|
660
807
|
publishAtomically,
|
|
661
808
|
probe,
|
|
809
|
+
porcelainPaths,
|
|
662
810
|
parseProvenance,
|
|
663
811
|
logicDigest,
|
|
664
812
|
importedSnapshotAsOf,
|
|
813
|
+
diffPorcelain,
|
|
665
814
|
decideCache,
|
|
666
815
|
checkReportStructure,
|
|
667
816
|
buildProvenance,
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
* `packages/` imports and no `Bun.*` globals, so the committed `.mjs` twin runs under bare `node`.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
+
import { spawnSync } from 'node:child_process';
|
|
30
31
|
import { createHash } from 'node:crypto';
|
|
31
32
|
import {
|
|
32
33
|
closeSync,
|
|
@@ -89,7 +90,7 @@ export interface CacheDecision {
|
|
|
89
90
|
|
|
90
91
|
// Frozen vocabulary from 0658's references/report-contract.md, kept local (not imported from
|
|
91
92
|
// `packages/`, not re-read from the skill) so this deterministic script is self-contained.
|
|
92
|
-
const
|
|
93
|
+
const REPORT_SECTIONS = [
|
|
93
94
|
'Scope and provenance',
|
|
94
95
|
'Executive summary',
|
|
95
96
|
'Baseline comparison',
|
|
@@ -99,6 +100,9 @@ const ELEVEN_SECTIONS = [
|
|
|
99
100
|
'Remediation options',
|
|
100
101
|
'Performance analysis',
|
|
101
102
|
'Workflow and process improvements',
|
|
103
|
+
// 0680 R5: standing report-only advisory slot (repeated tool-and-argument
|
|
104
|
+
// signatures propose no automatic interruption).
|
|
105
|
+
'Report-only advisories',
|
|
102
106
|
'Positive patterns',
|
|
103
107
|
'Evidence ledger',
|
|
104
108
|
];
|
|
@@ -113,6 +117,10 @@ const FINDING_FIELDS = [
|
|
|
113
117
|
'confidence',
|
|
114
118
|
'contradictions',
|
|
115
119
|
'evidenceAnchor',
|
|
120
|
+
// 0680 R1-R3: triage fields — the gate fails a finding missing any of them.
|
|
121
|
+
'severity',
|
|
122
|
+
'reproCommand',
|
|
123
|
+
'ownerSurface',
|
|
116
124
|
];
|
|
117
125
|
|
|
118
126
|
// ── 1. Semantic artifact digest ────────────────────────────────────────────────────────────
|
|
@@ -311,8 +319,19 @@ function escapeRe(s: string): string {
|
|
|
311
319
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
312
320
|
}
|
|
313
321
|
|
|
322
|
+
/** Closed finding categories (0686/I9). Kept next to FINDING_FIELDS — the gate parses them. */
|
|
323
|
+
const FINDING_CATEGORIES = [
|
|
324
|
+
'reliability',
|
|
325
|
+
'repetition',
|
|
326
|
+
'workflow',
|
|
327
|
+
'performance',
|
|
328
|
+
'coverage',
|
|
329
|
+
'telemetry',
|
|
330
|
+
'positive',
|
|
331
|
+
] as const;
|
|
332
|
+
|
|
314
333
|
/**
|
|
315
|
-
* Check a candidate report against the frozen report contract — the
|
|
334
|
+
* Check a candidate report against the frozen report contract — the twelve sections in order,
|
|
316
335
|
* the nine per-finding fields, no placeholders/TODOs/empty bodies, and evidence-ledger anchors.
|
|
317
336
|
*/
|
|
318
337
|
export function checkReportStructure(reportMarkdown: string): { ok: boolean; problems: string[] } {
|
|
@@ -321,7 +340,7 @@ export function checkReportStructure(reportMarkdown: string): { ok: boolean; pro
|
|
|
321
340
|
if (/TODO|PLACEHOLDER|FIXME|^\|\s*\|/im.test(reportMarkdown)) problems.push('placeholder-or-todo-present');
|
|
322
341
|
|
|
323
342
|
let lastIdx = -1;
|
|
324
|
-
for (const section of
|
|
343
|
+
for (const section of REPORT_SECTIONS) {
|
|
325
344
|
const re = new RegExp(`^#{2,3}\\s+${escapeRe(section)}\\s*$`, 'm');
|
|
326
345
|
const m = reportMarkdown.match(re);
|
|
327
346
|
if (m === null || (m.index ?? -1) <= lastIdx) {
|
|
@@ -331,12 +350,57 @@ export function checkReportStructure(reportMarkdown: string): { ok: boolean; pro
|
|
|
331
350
|
}
|
|
332
351
|
}
|
|
333
352
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
353
|
+
// 0680 R1-R3: findings are authored as bullet blocks under `## Findings` (one `### <title>`
|
|
354
|
+
// block per finding, key-value bullets). Scan each block that carries a stable key and fail
|
|
355
|
+
// it missing any triage field — including the three new ones. Without this scan a bullet
|
|
356
|
+
// finding never matches the legacy pipe-row regex and the triage gate was vacuous.
|
|
357
|
+
// 0686/I9: category parsing is scoped to that same body — an explicit category outside the
|
|
358
|
+
// closed vocabulary (FINDING_CATEGORIES) or a stable key whose first segment does so fails
|
|
359
|
+
// rather than passing vacuously.
|
|
360
|
+
const findingsIdx = reportMarkdown.search(/^##\s+Findings\s*$/im);
|
|
361
|
+
if (findingsIdx !== -1) {
|
|
362
|
+
const tail = reportMarkdown.slice(findingsIdx);
|
|
363
|
+
const nextSection = tail.slice(1).search(/^##\s+/im);
|
|
364
|
+
const findingsBody = nextSection === -1 ? tail : tail.slice(0, nextSection + 1);
|
|
365
|
+
|
|
366
|
+
const catAlt = FINDING_CATEGORIES.join('|');
|
|
367
|
+
const knownRows = findingsBody.match(new RegExp(`^\\|\\s*(?:${catAlt}):[^|]+`, 'gm')) ?? [];
|
|
368
|
+
const invalidRows = findingsBody.match(/^\|\s*([^|:\s][^|:]*):[^|]+/gm) ?? [];
|
|
369
|
+
for (const row of [...new Set([...knownRows, ...invalidRows])]) {
|
|
370
|
+
// 0686/I9: a pipe row outside the closed first-segment set fails by name instead of
|
|
371
|
+
// being skipped silently by the legacy regex.
|
|
372
|
+
const segMatch = row.match(/^\|\s*([^:|]+):/);
|
|
373
|
+
if (segMatch && !(FINDING_CATEGORIES as readonly string[]).includes(segMatch[1].trim())) {
|
|
374
|
+
problems.push(`finding-invalid-key-category:${segMatch[1].trim()}`);
|
|
375
|
+
}
|
|
376
|
+
// Case-insensitive on BOTH sides — camelCase triage fields otherwise never match a
|
|
377
|
+
// lowercased row.
|
|
378
|
+
const rowLower = row.toLowerCase();
|
|
379
|
+
for (const field of FINDING_FIELDS) {
|
|
380
|
+
if (!rowLower.includes(field.toLowerCase())) problems.push(`finding-missing-field:${field}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const blocks = findingsBody.split(/^###\s+/m).slice(1);
|
|
385
|
+
for (const block of blocks) {
|
|
386
|
+
if (!block.includes('`key`') && !/\|\s*key\s*:/.test(block)) continue;
|
|
387
|
+
for (const field of FINDING_FIELDS) {
|
|
388
|
+
if (!block.includes(field)) problems.push(`finding-missing-field:${field}`);
|
|
389
|
+
}
|
|
390
|
+
// Severity vocabulary is closed (0680 R1): P1/P2/P3 only (symbolic placeholders allowed).
|
|
391
|
+
if (!/(^|[\s`])P[123]([\s`.]|$)/.test(block) && !block.includes('symbolic-severity')) {
|
|
392
|
+
problems.push('finding-invalid-severity');
|
|
393
|
+
}
|
|
394
|
+
// 0686/I9 closed-category enforcement on bullet findings.
|
|
395
|
+
const catValue = block.match(/`category`\s*[:=]\s*`?([^`\n]+?)`?\s*(?:\n|$)/)?.[1];
|
|
396
|
+
if (catValue && !(FINDING_CATEGORIES as readonly string[]).includes(catValue.trim())) {
|
|
397
|
+
problems.push(`finding-invalid-category:${catValue.trim()}`);
|
|
398
|
+
}
|
|
399
|
+
const keyValue = block.match(/`key`\s*[:=]\s*`?([^`\n]+?)`?\s*(?:\n|$)/)?.[1] ?? '';
|
|
400
|
+
const firstSegment = keyValue.split(':')[0]?.trim() ?? '';
|
|
401
|
+
if (keyValue !== '' && !(FINDING_CATEGORIES as readonly string[]).includes(firstSegment)) {
|
|
402
|
+
problems.push(`finding-invalid-key-category:${firstSegment}`);
|
|
403
|
+
}
|
|
340
404
|
}
|
|
341
405
|
}
|
|
342
406
|
|
|
@@ -422,6 +486,66 @@ function localDay(tz: string, at: Date = new Date()): string {
|
|
|
422
486
|
}
|
|
423
487
|
}
|
|
424
488
|
|
|
489
|
+
/** Wall-clock offset of `tz` at instant `at`, ms east of UTC (0674 R2). */
|
|
490
|
+
function tzOffsetMs(tz: string, at: Date): number {
|
|
491
|
+
const parts = Object.fromEntries(
|
|
492
|
+
new Intl.DateTimeFormat('en-US', {
|
|
493
|
+
timeZone: tz,
|
|
494
|
+
hour12: false,
|
|
495
|
+
year: 'numeric',
|
|
496
|
+
month: '2-digit',
|
|
497
|
+
day: '2-digit',
|
|
498
|
+
hour: '2-digit',
|
|
499
|
+
minute: '2-digit',
|
|
500
|
+
second: '2-digit',
|
|
501
|
+
})
|
|
502
|
+
.formatToParts(at)
|
|
503
|
+
.filter((p) => p.type !== 'literal')
|
|
504
|
+
.map((p) => [p.type, p.value]),
|
|
505
|
+
);
|
|
506
|
+
const asUtc = Date.UTC(
|
|
507
|
+
Number(parts.year),
|
|
508
|
+
Number(parts.month) - 1,
|
|
509
|
+
Number(parts.day),
|
|
510
|
+
parts.hour === '24' ? 0 : Number(parts.hour),
|
|
511
|
+
Number(parts.minute),
|
|
512
|
+
Number(parts.second),
|
|
513
|
+
);
|
|
514
|
+
// Millisecond-truncate the comparison instant — Intl parts carry second precision, and an
|
|
515
|
+
// untruncated .999 epoch would leak into the offset (0674).
|
|
516
|
+
const atSec = Math.floor(at.getTime() / 1000) * 1000;
|
|
517
|
+
return asUtc - atSec;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** First instant of local calendar day `ymd` (two-pass so the offset guess survives DST edges). */
|
|
521
|
+
function zonedDayStart(tz: string, ymd: string): Date {
|
|
522
|
+
const [y, m, d] = ymd.split('-').map(Number);
|
|
523
|
+
const utcMidnight = Date.UTC(y ?? 1970, (m ?? 1) - 1, d ?? 1);
|
|
524
|
+
const guess = new Date(utcMidnight - tzOffsetMs(tz, new Date(utcMidnight)));
|
|
525
|
+
return new Date(utcMidnight - tzOffsetMs(tz, guess));
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** Instant rendered in `tz` wall clock with explicit offset: YYYY-MM-DDTHH:mm:ss.sss±HH:MM. */
|
|
529
|
+
function formatZonedIso(tz: string, at: Date): string {
|
|
530
|
+
const off = tzOffsetMs(tz, at);
|
|
531
|
+
const abs = Math.abs(off);
|
|
532
|
+
const pad = (n: number): string => String(n).padStart(2, '0');
|
|
533
|
+
const offStr = `${off < 0 ? '-' : '+'}${pad(Math.floor(abs / 3_600_000))}:${pad(Math.floor((abs % 3_600_000) / 60_000))}`;
|
|
534
|
+
// Shifting by the offset then formatting as UTC yields the zone's own wall clock.
|
|
535
|
+
return `${new Date(at.getTime() + off).toISOString().slice(0, 23)}${offStr}`;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** Inclusive bounds of one local calendar day; a DST day is 23/24/25h and both ends carry the real offset. */
|
|
539
|
+
function dayBounds(tz: string, ymd: string): { since: string; until: string } {
|
|
540
|
+
const start = zonedDayStart(tz, ymd);
|
|
541
|
+
// start + 30h always lands inside the NEXT calendar day regardless of 23/24/25h lengths.
|
|
542
|
+
const nextYmd = localDay(tz, new Date(start.getTime() + 30 * 3_600_000));
|
|
543
|
+
return {
|
|
544
|
+
since: formatZonedIso(tz, start),
|
|
545
|
+
until: formatZonedIso(tz, new Date(zonedDayStart(tz, nextYmd).getTime() - 1)),
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
425
549
|
/**
|
|
426
550
|
* Resolve the run's fixed paths once (0660 R4/R15): the skill directory beside the helper, the
|
|
427
551
|
* effective local date, and the publication target. Emitted as an env file so every downstream
|
|
@@ -433,14 +557,31 @@ export function resolvePaths(opts: {
|
|
|
433
557
|
date?: string;
|
|
434
558
|
output?: string;
|
|
435
559
|
now?: Date;
|
|
560
|
+
/** IANA zone override (test hook); defaults to the process zone. */
|
|
561
|
+
tz?: string;
|
|
562
|
+
mode?: string;
|
|
563
|
+
since?: string;
|
|
564
|
+
until?: string;
|
|
436
565
|
}): string {
|
|
437
566
|
const pluginRoot = opts.helper.replace(/\/scripts\/[^/]+$/, '');
|
|
438
567
|
const skill = `${pluginRoot}/skills/history-anatomy`;
|
|
439
|
-
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone ?? 'UTC';
|
|
568
|
+
const tz = opts.tz ?? Intl.DateTimeFormat().resolvedOptions().timeZone ?? 'UTC';
|
|
440
569
|
const date = opts.date !== undefined && opts.date !== '' ? opts.date : localDay(tz, opts.now ?? new Date());
|
|
441
570
|
const target =
|
|
442
571
|
opts.output !== undefined && opts.output !== '' ? opts.output : `${opts.reportDir}/${date}-history-anatomy.md`;
|
|
443
|
-
|
|
572
|
+
// 0674 R1/R2: daily bounds are derived here — date arithmetic in a named zone is exactly
|
|
573
|
+
// the "exceeds the shell composition threshold" case (ADR-069 R1), and DST makes a local
|
|
574
|
+
// day 23/24/25h. Ad-hoc (0674 R3): operator bounds pass through untouched, no baseline pair.
|
|
575
|
+
const adHoc = opts.mode === 'ad-hoc' && !!opts.since && !!opts.until;
|
|
576
|
+
let env = `HA_HELPER=${opts.helper}\nHA_SKILL=${skill}\nHA_TARGET=${target}\nHA_DATE=${date}\n`;
|
|
577
|
+
if (adHoc) {
|
|
578
|
+
return `${env}HA_SINCE=${opts.since}\nHA_UNTIL=${opts.until}\n`;
|
|
579
|
+
}
|
|
580
|
+
const current = dayBounds(tz, date);
|
|
581
|
+
const baseline = dayBounds(tz, localDay(tz, new Date(zonedDayStart(tz, date).getTime() - 12 * 3_600_000)));
|
|
582
|
+
env += `HA_SINCE=${current.since}\nHA_UNTIL=${current.until}\n`;
|
|
583
|
+
env += `HA_BASELINE_SINCE=${baseline.since}\nHA_BASELINE_UNTIL=${baseline.until}\n`;
|
|
584
|
+
return env;
|
|
444
585
|
}
|
|
445
586
|
|
|
446
587
|
export interface ProbeOptions {
|
|
@@ -658,7 +799,23 @@ export interface CacheCliResult {
|
|
|
658
799
|
stderr: string;
|
|
659
800
|
}
|
|
660
801
|
|
|
661
|
-
|
|
802
|
+
/** Parse a `git status --porcelain` text into its path set. */
|
|
803
|
+
export function porcelainPaths(text: string): Set<string> {
|
|
804
|
+
return new Set(
|
|
805
|
+
text
|
|
806
|
+
.split('\n')
|
|
807
|
+
.map((line) => line.replace(/^\S+\s+/, '').trim())
|
|
808
|
+
.filter((line) => line.length > 0),
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/** Paths present now but absent from the baseline and not declared outputs (0676 R3). */
|
|
813
|
+
export function diffPorcelain(before: string, now: string, expects: Set<string>): string[] {
|
|
814
|
+
const beforePaths = porcelainPaths(before);
|
|
815
|
+
return [...porcelainPaths(now)].filter((p) => !beforePaths.has(p) && !expects.has(p)).sort();
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const VALID_COMMANDS = 'digest, check, paths, assert-clean, probe, stamp, refresh, publish';
|
|
662
819
|
const PROBE_USAGE =
|
|
663
820
|
'<script> probe --artifact <a.json> --target <report.md> [--baseline <b.json>] [--mode daily|ad-hoc] ' +
|
|
664
821
|
'[--date <YYYY-MM-DD>] [--recompute true] [--out <prov.json>] [--skill-dir <d>] [--contract <f>] [--workflow <f>]';
|
|
@@ -716,13 +873,49 @@ export function runCacheCli(argv: string[]): CacheCliResult {
|
|
|
716
873
|
publishAtomically(a, b);
|
|
717
874
|
return { exitCode: 0, stdout: '', stderr: '' };
|
|
718
875
|
}
|
|
876
|
+
case 'assert-clean': {
|
|
877
|
+
// 0676 R3: fingerprint diff around a model stage. `--baseline` holds the
|
|
878
|
+
// pre-stage `git status --porcelain`; every path present now but absent there
|
|
879
|
+
// must be one of the stage's declared outputs, else exit 1 naming each.
|
|
880
|
+
const f = parseFlags(argv.slice(1));
|
|
881
|
+
if (f.baseline === undefined) {
|
|
882
|
+
return {
|
|
883
|
+
exitCode: 1,
|
|
884
|
+
stdout: '',
|
|
885
|
+
stderr: 'usage: <script> assert-clean --baseline <porcelain.txt> [--expect <path>]...\n',
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
const expects = new Set<string>();
|
|
889
|
+
for (const arg of argv.slice(1)) {
|
|
890
|
+
if (arg.startsWith('--expect=')) expects.add(arg.slice('--expect='.length));
|
|
891
|
+
}
|
|
892
|
+
let now: string;
|
|
893
|
+
try {
|
|
894
|
+
now =
|
|
895
|
+
spawnSync('git', ['status', '--porcelain'], {
|
|
896
|
+
encoding: 'utf8',
|
|
897
|
+
...(f.cwd !== undefined ? { cwd: f.cwd } : {}),
|
|
898
|
+
}).stdout ?? '';
|
|
899
|
+
} catch {
|
|
900
|
+
return { exitCode: 0, stdout: '', stderr: 'assert-clean: git unavailable; skipped\n' };
|
|
901
|
+
}
|
|
902
|
+
const undeclared = diffPorcelain(readFileSync(f.baseline, 'utf8'), now, expects);
|
|
903
|
+
if (undeclared.length > 0) {
|
|
904
|
+
return {
|
|
905
|
+
exitCode: 1,
|
|
906
|
+
stdout: '',
|
|
907
|
+
stderr: undeclared.map((p) => `undeclared write: ${p}\n`).join(''),
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
return { exitCode: 0, stdout: 'clean\n', stderr: '' };
|
|
911
|
+
}
|
|
719
912
|
case 'paths': {
|
|
720
913
|
const f = parseFlags(argv.slice(1));
|
|
721
914
|
if (f.helper === undefined || f.out === undefined) {
|
|
722
915
|
return {
|
|
723
916
|
exitCode: 1,
|
|
724
917
|
stdout: '',
|
|
725
|
-
stderr: 'usage: <script> paths --helper <p> --out <env> [--report-dir <d>] [--date <d>] [--output <p>]\n',
|
|
918
|
+
stderr: 'usage: <script> paths --helper <p> --out <env> [--report-dir <d>] [--date <d>] [--output <p>] [--mode <m>] [--since <s>] [--until <u>]\n',
|
|
726
919
|
};
|
|
727
920
|
}
|
|
728
921
|
writeFileSync(
|
|
@@ -732,6 +925,9 @@ export function runCacheCli(argv: string[]): CacheCliResult {
|
|
|
732
925
|
reportDir: f['report-dir'] ?? 'docs/report',
|
|
733
926
|
date: f.date,
|
|
734
927
|
output: f.output,
|
|
928
|
+
mode: f.mode,
|
|
929
|
+
since: f.since,
|
|
930
|
+
until: f.until,
|
|
735
931
|
}),
|
|
736
932
|
);
|
|
737
933
|
return { exitCode: 0, stdout: '', stderr: '' };
|
|
@@ -237,6 +237,36 @@ downstream task creation does not inherit an unactionable acceptance criterion:
|
|
|
237
237
|
|
|
238
238
|
The tag is a prompt to whoever turns findings into tasks: `[stale]` → drop, `[unverifiable]` →
|
|
239
239
|
reframe or defer, `[feasible]` → proceed. A finding without a tag is treated as `[feasible]`.
|
|
240
|
+
|
|
241
|
+
**Optional class tag (environment lens, task 0686).** A finding line may carry one closed class
|
|
242
|
+
tag — `environment` | `testee` | `waste` — positioned immediately after the em dash and distinct
|
|
243
|
+
from the trailing feasibility tag:
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
- **P2** — [environment] <what's wrong>. → **Action:** <concrete change>. (`file:line`, ~effort) `[feasible]`
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Omitting the class preserves the current line shape; untagged findings remain valid and the
|
|
250
|
+
protocol stays `sp:dogfood-testing@1.2` (the validator gains no required field; the cache-health
|
|
251
|
+
P3 above needs no class).
|
|
252
|
+
|
|
253
|
+
- `testee` — a defect in the testee's contract (the protocol the run grades). Bounded fix-mode
|
|
254
|
+
may repair it, unchanged.
|
|
255
|
+
- `environment` — a steering/placement/check-shaped observation projected from the
|
|
256
|
+
[environment-improvement lens](../../../references/environment-lens.md), which owns the
|
|
257
|
+
category table and the placement rule. **Proposal-only:** even under `--max-retry N`, bounded
|
|
258
|
+
retries never `Edit` or `Write` `AGENTS.md`, `CLAUDE.md`, skills, rules, numbered docs, or
|
|
259
|
+
plugin references for an environment-tagged finding — it stays a recommended action here.
|
|
260
|
+
- `waste` — a token/tool-waste diagnostic with no missing environment affordance behind it.
|
|
261
|
+
|
|
262
|
+
Class, not the cited file path, decides whether bounded fix may mutate. A session mistake a
|
|
263
|
+
linter, typechecker, test, or filesystem gate could have caught is `environment`, and its
|
|
264
|
+
recommended action proposes a new-or-tighter automated check — never another sentence in an
|
|
265
|
+
always-loaded steering file. Missed coding standards are `environment` owned by a review path
|
|
266
|
+
(`sp:code-verification`, `sp:code-review`, or pipeline review) — never the implementer skill;
|
|
267
|
+
navigation friction and dead always-loaded instructions are likewise `environment`, not
|
|
268
|
+
`testee`.
|
|
269
|
+
|
|
240
270
|
Severity scale:
|
|
241
271
|
|
|
242
272
|
- **P1** — blocks correct use or causes drift/wrong output; fix before shipping the testee.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: history-anatomy
|
|
3
|
-
description: "Independent owner of diagnostic interpretation over already-imported history — the daily/ad-hoc mode contract, a closed finding taxonomy, the
|
|
3
|
+
description: "Independent owner of diagnostic interpretation over already-imported history — the daily/ad-hoc mode contract, a closed finding taxonomy, the twelve-section report contract, and the enrich/validate rubrics. Triggers: history-anatomy, run the daily report, ad-hoc diagnosis, find issues over history."
|
|
4
4
|
license: Apache-2.0
|
|
5
5
|
version: 1.0.0
|
|
6
6
|
metadata:
|
|
@@ -37,7 +37,7 @@ fresh skill cannot be added to the BASELINE exemption map).
|
|
|
37
37
|
**Choose the mode first.** The single entry is `--mode daily|ad-hoc` (default `daily`):
|
|
38
38
|
`references/modes.md`.
|
|
39
39
|
|
|
40
|
-
**Then hold the report to the contract.** Every report must contain the
|
|
40
|
+
**Then hold the report to the contract.** Every report must contain the twelve sections and every
|
|
41
41
|
finding the full field set, with evidence rules and an explicit comparability verdict:
|
|
42
42
|
`references/report-contract.md`.
|
|
43
43
|
|
|
@@ -63,5 +63,5 @@ finding the full field set, with evidence rules and an explicit comparability ve
|
|
|
63
63
|
| Reference | Owns |
|
|
64
64
|
| --- | --- |
|
|
65
65
|
| [`references/modes.md`](references/modes.md) | The daily/ad-hoc mode matrix, bounds normalization, the DST-aware calendar-day rule, and the fail-loud message shape. |
|
|
66
|
-
| [`references/report-contract.md`](references/report-contract.md) | The
|
|
66
|
+
| [`references/report-contract.md`](references/report-contract.md) | The twelve sections (in order), the per-finding field set, the closed category vocabulary, the stable-key grammar, the evidence rules, comparison semantics, recurrence classes, and the positive-pattern / remediation standards. |
|
|
67
67
|
| [`references/operations.md`](references/operations.md) | The `enrich` and `validate` operation rubrics; neither launches a workflow. |
|