create-claude-cabinet 0.28.0 → 0.29.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.
- package/README.md +12 -0
- package/lib/cli.js +1 -0
- package/package.json +1 -1
- package/templates/cabinet/critique-contract.md +63 -0
- package/templates/cabinet/output-contract.md +19 -0
- package/templates/scripts/finding-schema.json +49 -0
- package/templates/scripts/merge-findings.js +23 -14
- package/templates/scripts/pib-db-lib.mjs +31 -8
- package/templates/scripts/pib-db-schema.sql +5 -1
- package/templates/scripts/triage-ui.html +178 -5
- package/templates/site-audit-runtime/package.json +1 -2
- package/templates/site-audit-runtime/src/checks/axe-core.mjs +17 -1
- package/templates/site-audit-runtime/src/checks/blacklight.mjs +7 -0
- package/templates/site-audit-runtime/src/checks/dns.mjs +12 -1
- package/templates/site-audit-runtime/src/checks/lighthouse.mjs +15 -3
- package/templates/site-audit-runtime/src/checks/linkinator.mjs +5 -0
- package/templates/site-audit-runtime/src/checks/meta-og.mjs +8 -1
- package/templates/site-audit-runtime/src/checks/nuclei.mjs +9 -1
- package/templates/site-audit-runtime/src/checks/observatory.mjs +7 -1
- package/templates/site-audit-runtime/src/checks/pa11y.mjs +7 -0
- package/templates/site-audit-runtime/src/checks/security-headers.mjs +5 -0
- package/templates/site-audit-runtime/src/checks/ssl-cert.mjs +15 -1
- package/templates/site-audit-runtime/src/checks/structured-data.mjs +7 -1
- package/templates/site-audit-runtime/src/checks/testssl.mjs +10 -1
- package/templates/site-audit-runtime/src/checks/unlighthouse.mjs +7 -1
- package/templates/site-audit-runtime/src/checks/website-carbon.mjs +7 -1
- package/templates/site-audit-runtime/src/cli.mjs +1 -1
- package/templates/site-audit-runtime/src/orchestrator.mjs +9 -4
- package/templates/site-audit-runtime/src/report.mjs +128 -17
- package/templates/site-audit-runtime/src/schema.mjs +2 -0
- package/templates/site-audit-runtime/tests/orchestrator.test.mjs +5 -1
- package/templates/skills/audit/SKILL.md +43 -1
- package/templates/skills/verify/SKILL.md +177 -27
- package/templates/skills/verify/install.sh +2 -1
- package/templates/skills/verify/phases/cleanup.md +38 -0
- package/templates/skills/verify/phases/post-run.md +16 -0
- package/templates/skills/verify/phases/run.md +24 -0
- package/templates/skills/verify/phases/scenario-template.md +30 -0
- package/templates/verify-runtime/CONVENTIONS.md +37 -0
- package/templates/verify-runtime/package-lock.json +165 -50
- package/templates/verify-runtime/package.json +2 -2
- package/templates/verify-runtime/src/baseline-steps.ts +1 -0
- package/templates/verify-runtime/src/demo-recorder.ts +46 -0
- package/templates/verify-runtime/src/human-verdict.ts +79 -14
- package/templates/verify-runtime/src/index.ts +20 -1
- package/templates/verify-runtime/src/launch-options.ts +28 -0
- package/templates/verify-runtime/src/output.ts +13 -0
- package/templates/verify-runtime/src/pause-on-failure.ts +53 -0
- package/templates/verify-runtime/src/trace.ts +15 -0
- package/templates/verify-runtime/src/world.ts +66 -16
- package/templates/verify-runtime/test/demo-mode.test.ts +85 -0
- package/templates/verify-runtime/test/trace.test.ts +40 -0
- package/templates/workflows/deliberative-audit.js +325 -0
package/README.md
CHANGED
|
@@ -90,6 +90,16 @@ few sessions, or before a release) to get a full review from every
|
|
|
90
90
|
relevant member. You don't need to audit every session. The cabinet
|
|
91
91
|
waits until called.
|
|
92
92
|
|
|
93
|
+
Each member is also a registered **agent type** — invoke any member
|
|
94
|
+
directly with `@cabinet-security`, `@cabinet-architecture`, etc.
|
|
95
|
+
|
|
96
|
+
When the Workflow tool is available, `/audit` runs a **deliberative
|
|
97
|
+
workflow**: Stage-1 members investigate, then Stage-2 critics
|
|
98
|
+
(anti-confirmation, QA, architecture) annotate findings — challenging
|
|
99
|
+
assumptions, adding context, or confirming. Findings arrive to triage
|
|
100
|
+
with the debate already attached. Optional `--rebuttal` mode lets
|
|
101
|
+
challenged members respond before triage.
|
|
102
|
+
|
|
93
103
|
Members are organized into **committees** — groups by concern, so you
|
|
94
104
|
can convene just the experts you need. Security review? Convene the
|
|
95
105
|
security committee. Performance concerns? Just the speed committee.
|
|
@@ -235,6 +245,8 @@ source code.
|
|
|
235
245
|
│ # (incl. pib-db-access.md, pib-db-triggers.md)
|
|
236
246
|
├── briefing/ # project briefing templates
|
|
237
247
|
├── hooks/ # git guardrails, telemetry
|
|
248
|
+
├── agents/ # generated agent-type wrappers (enables @cabinet-*)
|
|
249
|
+
├── workflows/ # deliberative-audit.js (two-stage audit workflow)
|
|
238
250
|
├── rules/ # enforcement pipeline
|
|
239
251
|
├── memory/ # pattern templates
|
|
240
252
|
└── settings.json # hook configuration
|
package/lib/cli.js
CHANGED
|
@@ -546,6 +546,7 @@ const MODULES = {
|
|
|
546
546
|
'scripts/triage-server.mjs', 'scripts/triage-ui.html',
|
|
547
547
|
'scripts/finding-schema.json', 'scripts/resolve-committees.cjs',
|
|
548
548
|
'scripts/review-server.mjs', 'scripts/review-ui.html',
|
|
549
|
+
'workflows/deliberative-audit.js', 'cabinet/critique-contract.md',
|
|
549
550
|
],
|
|
550
551
|
},
|
|
551
552
|
'lifecycle': {
|
package/package.json
CHANGED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Critique Contract — Stage-2 Audit Output
|
|
2
|
+
|
|
3
|
+
This contract defines how **Stage-2 critics** annotate Stage-1 findings
|
|
4
|
+
during a deliberative audit. Stage-1 members produce findings (see
|
|
5
|
+
`output-contract.md`). Stage-2 members review those findings through
|
|
6
|
+
their domain lens and produce annotations.
|
|
7
|
+
|
|
8
|
+
## Input
|
|
9
|
+
|
|
10
|
+
You receive a JSON array of findings from Stage-1 members. Each finding
|
|
11
|
+
has the fields defined in `output-contract.md`: id, cabinet-member,
|
|
12
|
+
severity, title, description, evidence, etc.
|
|
13
|
+
|
|
14
|
+
## Your Task
|
|
15
|
+
|
|
16
|
+
Review each finding through your domain lens. For findings that touch
|
|
17
|
+
your expertise, produce an annotation. For findings outside your domain,
|
|
18
|
+
stay silent — silence means "no objection from my perspective."
|
|
19
|
+
|
|
20
|
+
Default to no annotation. Only speak when you have something to add:
|
|
21
|
+
a factual correction, a challenge to an assumption, supporting evidence,
|
|
22
|
+
or additional context that changes how the finding should be interpreted.
|
|
23
|
+
|
|
24
|
+
## Annotation Types
|
|
25
|
+
|
|
26
|
+
| Type | When to use | Example |
|
|
27
|
+
|------|------------|---------|
|
|
28
|
+
| `challenge` | You disagree with the finding's conclusion or severity | "This input is already escaped by the ORM — the vulnerability described doesn't exist in the current implementation" |
|
|
29
|
+
| `support` | You have independent evidence confirming the finding | "The audit log also replicates to analytics DB — blast radius is wider than stated" |
|
|
30
|
+
| `context` | Additional information that changes interpretation | "This coupling is intentional — it's the auth boundary described in the architecture doc" |
|
|
31
|
+
| `correction` | A factual error in the finding | "The file path referenced was renamed in commit abc123 — the actual location is..." |
|
|
32
|
+
|
|
33
|
+
## Output Format
|
|
34
|
+
|
|
35
|
+
Return a JSON object matching this schema:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"annotations": [
|
|
40
|
+
{
|
|
41
|
+
"findingId": "security-0001",
|
|
42
|
+
"type": "challenge",
|
|
43
|
+
"text": "Your annotation here",
|
|
44
|
+
"severitySuggestion": "info"
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Fields:
|
|
51
|
+
- **findingId** (required): the `id` of the Stage-1 finding you're annotating
|
|
52
|
+
- **type** (required): one of `challenge`, `support`, `context`, `correction`
|
|
53
|
+
- **text** (required): your annotation — be specific and cite evidence
|
|
54
|
+
- **severitySuggestion** (optional): if you think the severity should change,
|
|
55
|
+
suggest the new level. Omit if the current severity is appropriate.
|
|
56
|
+
|
|
57
|
+
## Scope Rules
|
|
58
|
+
|
|
59
|
+
- Annotate only findings that intersect your domain expertise.
|
|
60
|
+
- You may annotate findings from any Stage-1 member, not just your own domain.
|
|
61
|
+
- Do not produce new findings — that's Stage 1's job.
|
|
62
|
+
- Do not repeat or rephrase a finding. Add to it or challenge it.
|
|
63
|
+
- If two findings contradict each other, annotate both to surface the tension.
|
|
@@ -146,3 +146,22 @@ Return valid JSON matching `scripts/finding-schema.json`.
|
|
|
146
146
|
|
|
147
147
|
Your response must be ONLY the JSON object — no markdown fences, no
|
|
148
148
|
commentary outside the JSON.
|
|
149
|
+
|
|
150
|
+
## Deliberation Fields (Two-Stage Audit)
|
|
151
|
+
|
|
152
|
+
When the audit runs in two-stage deliberative mode, findings may acquire
|
|
153
|
+
additional fields after initial creation:
|
|
154
|
+
|
|
155
|
+
- `status` — Set during deliberation. Values: `upheld` (no challenges),
|
|
156
|
+
`challenged` (critic disagreed), `modified` (author accepted critique),
|
|
157
|
+
`withdrawn` (author conceded), `rebutted` (author defended).
|
|
158
|
+
- `annotations[]` — Array of Stage-2 critic annotations. Each has
|
|
159
|
+
`cabinet-member`, `type` (challenge/support/context/correction),
|
|
160
|
+
`text`, and optional `severity-suggestion`.
|
|
161
|
+
- `rebuttal` — The original author's response to challenges. Has
|
|
162
|
+
`response` (withdraw/modify/defend) and `comment`.
|
|
163
|
+
|
|
164
|
+
**Stage-1 members:** Emit findings as normal using the schema above.
|
|
165
|
+
These deliberation fields are added later by the workflow.
|
|
166
|
+
|
|
167
|
+
**Stage-2 members:** Use `critique-contract.md` instead of this file.
|
|
@@ -75,6 +75,55 @@
|
|
|
75
75
|
"type": "array",
|
|
76
76
|
"items": { "type": "string" },
|
|
77
77
|
"description": "Categorization tags for filtering"
|
|
78
|
+
},
|
|
79
|
+
"status": {
|
|
80
|
+
"type": "string",
|
|
81
|
+
"enum": ["upheld", "challenged", "modified", "withdrawn", "rebutted"],
|
|
82
|
+
"description": "Set during Stage 2/3 deliberation. Absent for single-stage audits."
|
|
83
|
+
},
|
|
84
|
+
"annotations": {
|
|
85
|
+
"type": "array",
|
|
86
|
+
"items": {
|
|
87
|
+
"type": "object",
|
|
88
|
+
"required": ["cabinet-member", "type", "text"],
|
|
89
|
+
"properties": {
|
|
90
|
+
"cabinet-member": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"description": "Stage-2 critic who made this annotation"
|
|
93
|
+
},
|
|
94
|
+
"type": {
|
|
95
|
+
"type": "string",
|
|
96
|
+
"enum": ["challenge", "support", "context", "correction"],
|
|
97
|
+
"description": "challenge = disagrees, support = confirms, context = adds info, correction = factual fix"
|
|
98
|
+
},
|
|
99
|
+
"text": {
|
|
100
|
+
"type": "string",
|
|
101
|
+
"description": "The annotation content"
|
|
102
|
+
},
|
|
103
|
+
"severity-suggestion": {
|
|
104
|
+
"type": "string",
|
|
105
|
+
"enum": ["critical", "warn", "info", "idea"],
|
|
106
|
+
"description": "Critic's suggested severity change, if any"
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
"description": "Stage-2 critic annotations from deliberative audit. Absent for single-stage audits."
|
|
111
|
+
},
|
|
112
|
+
"rebuttal": {
|
|
113
|
+
"type": "object",
|
|
114
|
+
"properties": {
|
|
115
|
+
"response": {
|
|
116
|
+
"type": "string",
|
|
117
|
+
"enum": ["withdraw", "modify", "defend"],
|
|
118
|
+
"description": "Stage-1 member's response to challenges"
|
|
119
|
+
},
|
|
120
|
+
"comment": {
|
|
121
|
+
"type": "string",
|
|
122
|
+
"description": "Explanation of the response"
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
"required": ["response", "comment"],
|
|
126
|
+
"description": "Stage-1 member's response to Stage-2 challenges (opt-in rebuttal mode). Absent when no rebuttal requested or finding was not challenged."
|
|
78
127
|
}
|
|
79
128
|
}
|
|
80
129
|
}
|
|
@@ -90,23 +90,32 @@ for (const file of files) {
|
|
|
90
90
|
const timestamp = new Date().toISOString();
|
|
91
91
|
const runId = `run-${basename(runDir)}`;
|
|
92
92
|
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
positive: positiveCount,
|
|
104
|
-
...severityCounts,
|
|
105
|
-
},
|
|
106
|
-
byMember: memberCounts,
|
|
93
|
+
const meta = {
|
|
94
|
+
runId,
|
|
95
|
+
timestamp,
|
|
96
|
+
trigger: 'manual',
|
|
97
|
+
members: Object.keys(memberCounts),
|
|
98
|
+
counts: {
|
|
99
|
+
total: allFindings.length,
|
|
100
|
+
findings: allFindings.length - positiveCount,
|
|
101
|
+
positive: positiveCount,
|
|
102
|
+
...severityCounts,
|
|
107
103
|
},
|
|
104
|
+
byMember: memberCounts,
|
|
108
105
|
};
|
|
109
106
|
|
|
107
|
+
if (allFindings.some(f => f.annotations && f.annotations.length > 0)) {
|
|
108
|
+
meta.deliberation = {
|
|
109
|
+
annotatedCount: allFindings.filter(f => f.annotations && f.annotations.length > 0).length,
|
|
110
|
+
challengedCount: allFindings.filter(f => f.status === 'challenged' || f.status === 'rebutted').length,
|
|
111
|
+
upheldCount: allFindings.filter(f => f.status === 'upheld').length,
|
|
112
|
+
withdrawnCount: allFindings.filter(f => f.status === 'withdrawn').length,
|
|
113
|
+
modifiedCount: allFindings.filter(f => f.status === 'modified').length,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const summary = { findings: allFindings, meta };
|
|
118
|
+
|
|
110
119
|
const summaryPath = join(runDir, 'run-summary.json');
|
|
111
120
|
writeFileSync(summaryPath, JSON.stringify(summary, null, 2));
|
|
112
121
|
console.log(`\nMerged ${allFindings.length} findings → ${summaryPath}`);
|
|
@@ -285,21 +285,44 @@ export function ingestFindings(db, { runDir }) {
|
|
|
285
285
|
VALUES (?, ?, ?, ?, ?)
|
|
286
286
|
`).run(runId, dateStr, timestamp, data.meta?.trigger || 'manual', data.findings?.length || 0);
|
|
287
287
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
288
|
+
// Try the full INSERT with deliberation columns first; fall back to the
|
|
289
|
+
// legacy shape for existing databases that haven't added them yet.
|
|
290
|
+
let insert;
|
|
291
|
+
let hasDeliberationCols = true;
|
|
292
|
+
try {
|
|
293
|
+
insert = db.prepare(`
|
|
294
|
+
INSERT OR REPLACE INTO audit_findings
|
|
295
|
+
(id, run_id, cabinet_member, severity, title, description, assumption,
|
|
296
|
+
evidence, question, file, line, suggested_fix, auto_fixable, type,
|
|
297
|
+
status, annotations, rebuttal)
|
|
298
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
299
|
+
`);
|
|
300
|
+
} catch {
|
|
301
|
+
hasDeliberationCols = false;
|
|
302
|
+
insert = db.prepare(`
|
|
303
|
+
INSERT OR REPLACE INTO audit_findings
|
|
304
|
+
(id, run_id, cabinet_member, severity, title, description, assumption,
|
|
305
|
+
evidence, question, file, line, suggested_fix, auto_fixable, type)
|
|
306
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
307
|
+
`);
|
|
308
|
+
}
|
|
294
309
|
|
|
295
310
|
let count = 0;
|
|
296
311
|
for (const f of (data.findings || [])) {
|
|
297
|
-
|
|
312
|
+
const base = [
|
|
298
313
|
f.id, runId, f['cabinet-member'], f.severity, f.title,
|
|
299
314
|
f.description || null, f.assumption || null, f.evidence || null,
|
|
300
315
|
f.question || null, f.file || null, f.line || null,
|
|
301
316
|
f.suggestedFix || null, f.autoFixable ? 1 : 0, f.type || 'finding'
|
|
302
|
-
|
|
317
|
+
];
|
|
318
|
+
if (hasDeliberationCols) {
|
|
319
|
+
base.push(
|
|
320
|
+
f.status || null,
|
|
321
|
+
f.annotations ? JSON.stringify(f.annotations) : null,
|
|
322
|
+
f.rebuttal ? JSON.stringify(f.rebuttal) : null
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
insert.run(...base);
|
|
303
326
|
count++;
|
|
304
327
|
}
|
|
305
328
|
return { count, runId, message: `Ingested ${count} findings from ${runDir} (run: ${runId})` };
|
|
@@ -66,7 +66,11 @@ CREATE TABLE IF NOT EXISTS audit_findings (
|
|
|
66
66
|
CHECK(triage_status IN ('open','approved','rejected','deferred','fixed','archived')),
|
|
67
67
|
triage_notes TEXT,
|
|
68
68
|
triaged_at TEXT,
|
|
69
|
-
fix_description TEXT
|
|
69
|
+
fix_description TEXT,
|
|
70
|
+
-- Deliberation fields (two-stage audit). Absent for single-stage audits.
|
|
71
|
+
status TEXT CHECK(status IS NULL OR status IN ('upheld','challenged','modified','withdrawn','rebutted')),
|
|
72
|
+
annotations TEXT, -- JSON array of Stage-2 critic annotations
|
|
73
|
+
rebuttal TEXT -- JSON object: Stage-1 member's response to challenges
|
|
70
74
|
);
|
|
71
75
|
|
|
72
76
|
-- Append-only history of trigger-condition evaluations.
|
|
@@ -63,11 +63,17 @@
|
|
|
63
63
|
|
|
64
64
|
/* Finding rows */
|
|
65
65
|
.finding {
|
|
66
|
-
padding: 10px 14px;
|
|
66
|
+
padding: 10px 14px 10px 18px;
|
|
67
67
|
border-top: 1px solid #2c2e33;
|
|
68
|
+
border-left: 4px solid transparent;
|
|
68
69
|
transition: background 0.1s;
|
|
69
70
|
}
|
|
70
71
|
.finding:hover { background: #25262b; }
|
|
72
|
+
.finding[data-status="challenged"] { border-left-color: #e03131; background: #1e1a1a; }
|
|
73
|
+
.finding[data-status="rebutted"] { border-left-color: #7048e8; background: #1a1820; }
|
|
74
|
+
.finding[data-status="upheld"] { border-left-color: #2b8a3e; }
|
|
75
|
+
.finding[data-status="modified"] { border-left-color: #f08c00; }
|
|
76
|
+
.finding[data-status="withdrawn"] { border-left-color: #868e96; }
|
|
71
77
|
.finding-top {
|
|
72
78
|
display: flex;
|
|
73
79
|
align-items: flex-start;
|
|
@@ -93,11 +99,119 @@
|
|
|
93
99
|
.finding-id { color: #5c5f66; font-size: 11px; font-family: monospace; flex-shrink: 0; }
|
|
94
100
|
|
|
95
101
|
.commentary {
|
|
96
|
-
color: #
|
|
102
|
+
color: #c1c2c5;
|
|
103
|
+
font-size: 13px;
|
|
104
|
+
margin: 6px 0 8px 0;
|
|
105
|
+
padding: 0;
|
|
106
|
+
line-height: 1.6;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.finding-detail {
|
|
110
|
+
margin: 6px 0;
|
|
111
|
+
padding: 8px 10px;
|
|
112
|
+
background: #1a1b1e;
|
|
113
|
+
border-radius: 4px;
|
|
97
114
|
font-size: 12px;
|
|
98
|
-
margin: 4px 0 8px 0;
|
|
99
|
-
padding-left: 2px;
|
|
100
115
|
line-height: 1.6;
|
|
116
|
+
color: #c1c2c5;
|
|
117
|
+
}
|
|
118
|
+
.finding-detail p { margin: 0 0 6px 0; }
|
|
119
|
+
.finding-detail p:last-child { margin-bottom: 0; }
|
|
120
|
+
.detail-label {
|
|
121
|
+
color: #909296;
|
|
122
|
+
font-size: 11px;
|
|
123
|
+
font-weight: 600;
|
|
124
|
+
text-transform: uppercase;
|
|
125
|
+
letter-spacing: 0.5px;
|
|
126
|
+
margin-bottom: 2px;
|
|
127
|
+
}
|
|
128
|
+
.detail-section { margin-bottom: 8px; }
|
|
129
|
+
.detail-section:last-child { margin-bottom: 0; }
|
|
130
|
+
.detail-fix {
|
|
131
|
+
border-left: 2px solid #2b8a3e;
|
|
132
|
+
padding-left: 8px;
|
|
133
|
+
margin-top: 4px;
|
|
134
|
+
}
|
|
135
|
+
.detail-question {
|
|
136
|
+
border-left: 2px solid #f08c00;
|
|
137
|
+
padding-left: 8px;
|
|
138
|
+
margin-top: 4px;
|
|
139
|
+
font-style: italic;
|
|
140
|
+
}
|
|
141
|
+
.annotation {
|
|
142
|
+
margin: 6px 0 6px 20px;
|
|
143
|
+
padding: 7px 12px;
|
|
144
|
+
border-left: 3px solid;
|
|
145
|
+
border-radius: 0 4px 4px 0;
|
|
146
|
+
background: #1e1f22;
|
|
147
|
+
font-size: 12px;
|
|
148
|
+
line-height: 1.5;
|
|
149
|
+
color: #c1c2c5;
|
|
150
|
+
}
|
|
151
|
+
.annotation.ann-type-challenge { border-left-color: #e03131; }
|
|
152
|
+
.annotation.ann-type-support { border-left-color: #2b8a3e; }
|
|
153
|
+
.annotation.ann-type-context { border-left-color: #1c7ed6; }
|
|
154
|
+
.annotation.ann-type-correction { border-left-color: #f08c00; }
|
|
155
|
+
.annotation-header {
|
|
156
|
+
display: flex;
|
|
157
|
+
gap: 6px;
|
|
158
|
+
align-items: center;
|
|
159
|
+
margin-bottom: 2px;
|
|
160
|
+
font-size: 11px;
|
|
161
|
+
}
|
|
162
|
+
.annotation-member { color: #909296; font-family: monospace; }
|
|
163
|
+
.ann-badge {
|
|
164
|
+
padding: 1px 5px;
|
|
165
|
+
border-radius: 3px;
|
|
166
|
+
font-size: 10px;
|
|
167
|
+
font-weight: 600;
|
|
168
|
+
text-transform: uppercase;
|
|
169
|
+
}
|
|
170
|
+
.ann-challenge { background: #e03131; color: #fff; }
|
|
171
|
+
.ann-support { background: #2b8a3e; color: #fff; }
|
|
172
|
+
.ann-context { background: #1c7ed6; color: #fff; }
|
|
173
|
+
.ann-correction { background: #f08c00; color: #fff; }
|
|
174
|
+
.finding-rebuttal {
|
|
175
|
+
margin: 6px 0 6px 40px;
|
|
176
|
+
padding: 7px 12px;
|
|
177
|
+
border-left: 3px solid #7048e8;
|
|
178
|
+
border-radius: 0 4px 4px 0;
|
|
179
|
+
background: #1e1f22;
|
|
180
|
+
font-size: 12px;
|
|
181
|
+
color: #c1c2c5;
|
|
182
|
+
}
|
|
183
|
+
.rebuttal-header { color: #7048e8; font-size: 11px; font-weight: 600; margin-bottom: 2px; }
|
|
184
|
+
.status-badge {
|
|
185
|
+
padding: 1px 5px;
|
|
186
|
+
border-radius: 3px;
|
|
187
|
+
font-size: 10px;
|
|
188
|
+
font-weight: 600;
|
|
189
|
+
margin-left: 6px;
|
|
190
|
+
}
|
|
191
|
+
.status-challenged { background: #e03131; color: #fff; }
|
|
192
|
+
.status-upheld { background: #2b8a3e; color: #fff; }
|
|
193
|
+
.status-modified { background: #f08c00; color: #fff; }
|
|
194
|
+
.status-withdrawn { background: #868e96; color: #fff; }
|
|
195
|
+
.status-rebutted { background: #7048e8; color: #fff; }
|
|
196
|
+
.annotation-section-label {
|
|
197
|
+
color: #5c5f66;
|
|
198
|
+
font-size: 11px;
|
|
199
|
+
font-weight: 600;
|
|
200
|
+
text-transform: uppercase;
|
|
201
|
+
letter-spacing: 0.5px;
|
|
202
|
+
margin: 8px 0 4px 16px;
|
|
203
|
+
}
|
|
204
|
+
.no-objections {
|
|
205
|
+
color: #5c5f66;
|
|
206
|
+
font-size: 11px;
|
|
207
|
+
font-style: italic;
|
|
208
|
+
margin: 6px 0 2px 16px;
|
|
209
|
+
padding: 4px 0;
|
|
210
|
+
}
|
|
211
|
+
.critique-zone {
|
|
212
|
+
margin-top: 10px;
|
|
213
|
+
padding-top: 10px;
|
|
214
|
+
border-top: 1px dashed #2c2e33;
|
|
101
215
|
}
|
|
102
216
|
|
|
103
217
|
.controls {
|
|
@@ -398,12 +512,15 @@
|
|
|
398
512
|
group.className = 'member-group';
|
|
399
513
|
|
|
400
514
|
// Header
|
|
515
|
+
const challenged = items.filter(f => f.status === 'challenged' || f.status === 'rebutted').length;
|
|
516
|
+
|
|
401
517
|
const header = document.createElement('div');
|
|
402
518
|
header.className = 'member-header';
|
|
403
519
|
header.innerHTML = `
|
|
404
520
|
<span class="member-name">${member} (${items.length})</span>
|
|
405
521
|
<span class="member-stats">
|
|
406
522
|
${triaged}/${items.length} triaged
|
|
523
|
+
${challenged ? ` · <span style="color:#e03131">${challenged} contested</span>` : ''}
|
|
407
524
|
</span>
|
|
408
525
|
`;
|
|
409
526
|
header.addEventListener('click', () => {
|
|
@@ -418,15 +535,71 @@
|
|
|
418
535
|
items.forEach(f => {
|
|
419
536
|
const row = document.createElement('div');
|
|
420
537
|
row.className = 'finding';
|
|
538
|
+
if (f.status) row.dataset.status = f.status;
|
|
421
539
|
const v = verdicts[f.id]?.verdict || '';
|
|
422
540
|
|
|
541
|
+
// --- Finding detail section ---
|
|
542
|
+
const detailParts = [];
|
|
543
|
+
if (f.description) {
|
|
544
|
+
detailParts.push(`<div class="detail-section"><div class="detail-label">Description</div><p>${esc(f.description)}</p></div>`);
|
|
545
|
+
}
|
|
546
|
+
if (f.evidence) {
|
|
547
|
+
detailParts.push(`<div class="detail-section"><div class="detail-label">Evidence</div><p>${esc(f.evidence)}</p></div>`);
|
|
548
|
+
}
|
|
549
|
+
if (f.assumption) {
|
|
550
|
+
detailParts.push(`<div class="detail-section"><div class="detail-label">Assumption</div><p>${esc(f.assumption)}</p></div>`);
|
|
551
|
+
}
|
|
552
|
+
if (f.question) {
|
|
553
|
+
detailParts.push(`<div class="detail-section"><div class="detail-label">Open question</div><div class="detail-question">${esc(f.question)}</div></div>`);
|
|
554
|
+
}
|
|
555
|
+
if (f.suggestedFix) {
|
|
556
|
+
detailParts.push(`<div class="detail-section"><div class="detail-label">Suggested fix</div><div class="detail-fix">${esc(f.suggestedFix)}</div></div>`);
|
|
557
|
+
}
|
|
558
|
+
if (f.file) {
|
|
559
|
+
detailParts.push(`<div class="detail-section"><div class="detail-label">Location</div><p>${esc(f.file)}${f.line ? ':' + f.line : ''}</p></div>`);
|
|
560
|
+
}
|
|
561
|
+
const detailHtml = detailParts.length > 0
|
|
562
|
+
? `<div class="finding-detail">${detailParts.join('')}</div>`
|
|
563
|
+
: '';
|
|
564
|
+
|
|
565
|
+
// --- Annotations section ---
|
|
566
|
+
const anns = f.annotations || [];
|
|
567
|
+
let annHtml = '';
|
|
568
|
+
if (anns.length > 0) {
|
|
569
|
+
annHtml = `<div class="annotation-section-label">Stage-2 critique (${anns.length})</div>` +
|
|
570
|
+
anns.map(a => `
|
|
571
|
+
<div class="annotation ann-type-${a.type || 'context'}">
|
|
572
|
+
<div class="annotation-header">
|
|
573
|
+
<span class="annotation-member">${esc(a['cabinet-member'] || '')}</span>
|
|
574
|
+
<span class="ann-badge ann-${a.type || 'context'}">${esc(a.type || 'context')}</span>
|
|
575
|
+
${a['severity-suggestion'] ? `<span class="badge badge-${a['severity-suggestion']}">→ ${esc(a['severity-suggestion'])}</span>` : ''}
|
|
576
|
+
</div>
|
|
577
|
+
${esc(a.text || '')}
|
|
578
|
+
</div>`).join('');
|
|
579
|
+
} else if (f.status === 'upheld') {
|
|
580
|
+
const allAnns = findings.flatMap(ff => (ff.annotations || []).map(a => a['cabinet-member']));
|
|
581
|
+
const critics = [...new Set(allAnns)];
|
|
582
|
+
const criticList = critics.length > 0 ? critics.join(', ') : 'Stage-2 critics';
|
|
583
|
+
annHtml = `<div class="no-objections">Reviewed by ${esc(criticList)} — no objections</div>`;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const rebHtml = f.rebuttal ? `
|
|
587
|
+
<div class="annotation-section-label">Author response</div>
|
|
588
|
+
<div class="finding-rebuttal">
|
|
589
|
+
<div class="rebuttal-header">${esc(f['cabinet-member'])} — ${esc(f.rebuttal.response)}</div>
|
|
590
|
+
${esc(f.rebuttal.comment || '')}
|
|
591
|
+
</div>` : '';
|
|
592
|
+
const statusHtml = f.status ? `<span class="status-badge status-${f.status}">${esc(f.status)}</span>` : '';
|
|
593
|
+
|
|
423
594
|
row.innerHTML = `
|
|
424
595
|
<div class="finding-top">
|
|
425
596
|
<span class="badge badge-${f.severity}">${f.severity}</span>
|
|
426
|
-
<span class="finding-title">${esc(f.title)}</span>
|
|
597
|
+
<span class="finding-title">${esc(f.title)}${statusHtml}</span>
|
|
427
598
|
<span class="finding-id">${esc(f.id)}</span>
|
|
428
599
|
</div>
|
|
429
600
|
${f.commentary ? `<div class="commentary">${esc(f.commentary)}</div>` : ''}
|
|
601
|
+
${detailHtml}
|
|
602
|
+
<div class="critique-zone">${annHtml}${rebHtml}</div>
|
|
430
603
|
<div class="controls">
|
|
431
604
|
<button class="verdict-btn ${v === 'fix' ? 'sel-fix' : ''}" data-id="${f.id}" data-v="fix">Fix</button>
|
|
432
605
|
<button class="verdict-btn ${v === 'defer' ? 'sel-defer' : ''}" data-id="${f.id}" data-v="defer">Defer</button>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-cabinet/site-audit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Comprehensive deployed-site quality audit engine for Claude Cabinet. Runs checks across performance, accessibility, security, SEO, content, DNS, and privacy against a deployed URL; single-site and comparison modes; standalone HTML report.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@axe-core/cli": "^4.10.0",
|
|
21
21
|
"@mdn/mdn-http-observatory": "^1.6.0",
|
|
22
|
-
"@themarkup/blacklight-collector": "^1.0.0",
|
|
23
22
|
"lighthouse": "^12.0.0",
|
|
24
23
|
"linkinator": "^7.0.0",
|
|
25
24
|
"pa11y": "^9.0.0",
|
|
@@ -18,7 +18,17 @@ export function normalize(raw, durationMs) {
|
|
|
18
18
|
}
|
|
19
19
|
let data;
|
|
20
20
|
try { data = JSON.parse(raw.stdout); } catch {
|
|
21
|
-
|
|
21
|
+
// @axe-core/cli may output non-JSON prefix lines before the JSON array;
|
|
22
|
+
// strip lines until we find the opening bracket.
|
|
23
|
+
const lines = (raw.stdout || '').split('\n');
|
|
24
|
+
const jsonStart = lines.findIndex(l => l.trimStart().startsWith('[') || l.trimStart().startsWith('{'));
|
|
25
|
+
if (jsonStart >= 0) {
|
|
26
|
+
try { data = JSON.parse(lines.slice(jsonStart).join('\n')); } catch {
|
|
27
|
+
return { checkId, tool, status: 'error', score: null, grade: null, severity: null, findings: [], durationMs, reason: 'failed to parse axe-core JSON' };
|
|
28
|
+
}
|
|
29
|
+
} else {
|
|
30
|
+
return { checkId, tool, status: 'error', score: null, grade: null, severity: null, findings: [], durationMs, reason: 'failed to parse axe-core JSON' };
|
|
31
|
+
}
|
|
22
32
|
}
|
|
23
33
|
|
|
24
34
|
const violations = Array.isArray(data) ? data.flatMap(p => p.violations || []) : (data.violations || []);
|
|
@@ -34,8 +44,14 @@ export function normalize(raw, durationMs) {
|
|
|
34
44
|
return (o[f.severity] ?? 3) < (o[w] ?? 3) ? f.severity : w;
|
|
35
45
|
}, 'info') : null;
|
|
36
46
|
|
|
47
|
+
const pages = Array.isArray(data) ? data.length : 1;
|
|
48
|
+
const passSummary = findings.length === 0
|
|
49
|
+
? `No WCAG AA violations across ${pages} page${pages !== 1 ? 's' : ''}`
|
|
50
|
+
: undefined;
|
|
51
|
+
|
|
37
52
|
return {
|
|
38
53
|
checkId, tool, status: findings.length === 0 ? 'pass' : 'fail',
|
|
39
54
|
score: null, grade: null, severity: worstSev, findings, durationMs,
|
|
55
|
+
...(passSummary && { passSummary }),
|
|
40
56
|
};
|
|
41
57
|
}
|
|
@@ -72,8 +72,15 @@ export function normalize(raw, durationMs) {
|
|
|
72
72
|
}, 'info') : null;
|
|
73
73
|
|
|
74
74
|
const hasSerious = findings.some(f => f.severity === 'serious' || f.severity === 'critical');
|
|
75
|
+
const passSummary = !hasSerious
|
|
76
|
+
? (findings.length === 0
|
|
77
|
+
? 'No trackers, fingerprinters, or session recorders detected'
|
|
78
|
+
: `No serious trackers detected (${findings.length} low-severity item${findings.length !== 1 ? 's' : ''} only)`)
|
|
79
|
+
: undefined;
|
|
80
|
+
|
|
75
81
|
return {
|
|
76
82
|
checkId, tool, status: hasSerious ? 'fail' : 'pass',
|
|
77
83
|
score: null, grade: null, severity: worstSev, findings, durationMs,
|
|
84
|
+
...(passSummary && { passSummary }),
|
|
78
85
|
};
|
|
79
86
|
}
|
|
@@ -53,13 +53,24 @@ export function normalize(raw, durationMs) {
|
|
|
53
53
|
const passing = total - findings.filter(f => f.severity !== 'info').length;
|
|
54
54
|
const score = Math.round((passing / total) * 100);
|
|
55
55
|
|
|
56
|
+
const isPass = !findings.some(f => f.severity !== 'info');
|
|
57
|
+
const passed = [];
|
|
58
|
+
if (!findings.some(f => f.message.includes('DNSSEC'))) passed.push('DNSSEC');
|
|
59
|
+
if (!findings.some(f => f.message.includes('SPF'))) passed.push('SPF');
|
|
60
|
+
if (!findings.some(f => f.message.includes('DMARC'))) passed.push('DMARC');
|
|
61
|
+
if (httpVersion && (httpVersion.startsWith('2') || httpVersion.startsWith('3'))) passed.push(`HTTP/${httpVersion}`);
|
|
62
|
+
const passSummary = isPass
|
|
63
|
+
? `${passed.join(', ')} verified`
|
|
64
|
+
: undefined;
|
|
65
|
+
|
|
56
66
|
return {
|
|
57
|
-
checkId, tool, status:
|
|
67
|
+
checkId, tool, status: isPass ? 'pass' : 'fail',
|
|
58
68
|
score, grade: null,
|
|
59
69
|
severity: findings.length ? findings.reduce((w, f) => {
|
|
60
70
|
const o = { critical: 0, serious: 1, moderate: 2, info: 3 };
|
|
61
71
|
return (o[f.severity] ?? 3) < (o[w] ?? 3) ? f.severity : w;
|
|
62
72
|
}, 'info') : null,
|
|
63
73
|
findings, durationMs,
|
|
74
|
+
...(passSummary && { passSummary }),
|
|
64
75
|
};
|
|
65
76
|
}
|
|
@@ -39,11 +39,17 @@ export function normalize(raw, durationMs) {
|
|
|
39
39
|
const audits = data.audits || {};
|
|
40
40
|
for (const [id, audit] of Object.entries(audits)) {
|
|
41
41
|
if (audit.score !== null && audit.score < 0.5 && audit.title) {
|
|
42
|
-
|
|
42
|
+
const f = {
|
|
43
43
|
severity: audit.score === 0 ? 'serious' : 'moderate',
|
|
44
44
|
message: audit.title,
|
|
45
45
|
context: audit.displayValue || undefined,
|
|
46
|
-
}
|
|
46
|
+
};
|
|
47
|
+
if (audit.details?.items?.length) {
|
|
48
|
+
f.url = audit.details.items.slice(0, 5).map(
|
|
49
|
+
item => item.url || item.source?.url || item.node?.selector || ''
|
|
50
|
+
).filter(Boolean).join(', ') || undefined;
|
|
51
|
+
}
|
|
52
|
+
findings.push(f);
|
|
47
53
|
}
|
|
48
54
|
}
|
|
49
55
|
|
|
@@ -52,9 +58,15 @@ export function normalize(raw, durationMs) {
|
|
|
52
58
|
return (order[f.severity] ?? 3) < (order[w] ?? 3) ? f.severity : w;
|
|
53
59
|
}, 'info') : null;
|
|
54
60
|
|
|
61
|
+
const details = { categories: scores };
|
|
62
|
+
const passSummary = Object.entries(scores)
|
|
63
|
+
.map(([k, v]) => `${k.replace(/-/g, ' ')}: ${v}`)
|
|
64
|
+
.join(', ');
|
|
65
|
+
|
|
55
66
|
return {
|
|
56
67
|
checkId, tool, status: avg !== null && avg >= 50 ? 'pass' : 'fail',
|
|
57
|
-
score: avg, grade: scoreToGrade(avg), severity: worstSev, findings,
|
|
68
|
+
score: avg, grade: scoreToGrade(avg), severity: worstSev, findings,
|
|
69
|
+
details, passSummary, durationMs,
|
|
58
70
|
};
|
|
59
71
|
}
|
|
60
72
|
|