@event4u/agent-config 10.1.0 → 10.3.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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +110 -167
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/reference-repo.md +62 -0
- package/dist/agent-src/commands/roadmap/create.md +4 -1
- package/dist/agent-src/commands/roadmap/next.md +38 -0
- package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +22 -1
- package/dist/agent-src/contexts/contracts/frugality-charter.md +1 -1
- package/dist/agent-src/personas/advisors/first-principles.md +6 -0
- package/dist/agent-src/rules/active-remediation.md +53 -2
- package/dist/agent-src/rules/code-provenance.md +66 -4
- package/dist/agent-src/rules/content-quoting-floor.md +6 -0
- package/dist/agent-src/rules/context-hygiene.md +39 -0
- package/dist/agent-src/rules/design-fidelity.md +49 -3
- package/dist/agent-src/rules/token-budget-discipline.md +3 -2
- package/dist/agent-src/rules/token-efficiency.md +22 -0
- package/dist/agent-src/skills/ai-council/SKILL.md +28 -6
- package/dist/agent-src/skills/condense-memory/SKILL.md +1 -1
- package/dist/agent-src/skills/design-review/references/verification-automation.md +23 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +27 -3
- package/dist/agent-src/skills/judge-synthesis/SKILL.md +30 -0
- package/dist/agent-src/skills/learning-to-rule-or-skill/SKILL.md +11 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +9 -9
- package/dist/agent-src/skills/skill-writing/SKILL.md +4 -0
- package/dist/agent-src/skills/token-optimizer/SKILL.md +5 -1
- package/dist/agent-src/templates/agent-settings.md +12 -28
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +2 -2
- package/dist/agent-src/templates/persona.md +26 -0
- package/dist/agent-src/templates/roadmaps.md +27 -7
- package/dist/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +0 -1
- package/dist/agent-src/templates/skill.md +35 -1
- package/dist/cli/commands/commands.candidates.test.js +257 -0
- package/dist/cli/commands/commands.candidates.test.js.map +1 -0
- package/dist/cli/commands/commands.js +199 -0
- package/dist/cli/commands/commands.js.map +1 -1
- package/dist/cli/main.js +1 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/registry.js +2 -2
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-3JUTT26I.js → chunk-6ENSXSBW.js} +1 -1
- package/dist/cli-delegate/{chunk-TVGXH7VA.js → chunk-6LOPHJLD.js} +1 -1
- package/dist/cli-delegate/{chunk-XT2YTJCZ.js → chunk-AYKQGIBG.js} +4 -0
- package/dist/cli-delegate/{chunk-HJSKTMIF.js → chunk-BV3WR67D.js} +3 -19
- package/dist/cli-delegate/{chunk-HTIU3W56.js → chunk-BWMXLD4B.js} +6 -2
- package/dist/cli-delegate/{chunk-2H5SE7DU.js → chunk-FV34EGEX.js} +166 -39
- package/dist/cli-delegate/{chunk-XTC5ARB6.js → chunk-IR3IVRVC.js} +7 -2
- package/dist/cli-delegate/{chunk-LRXJK6GN.js → chunk-JMQZ7MM5.js} +1 -1
- package/dist/cli-delegate/{chunk-NX5AXSLD.js → chunk-X3VOARZD.js} +3 -3
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +881 -204
- package/dist/cli-delegate/cmd_doctor.js +3 -3
- package/dist/cli-delegate/cmd_explain.js +2 -2
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_handoff.js +6 -5
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +44 -9
- package/dist/cli-delegate/cmd_settings_get.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_settings_set.js +2 -2
- package/dist/cli-delegate/cmd_sync.js +4 -4
- package/dist/cli-delegate/cmd_uninstall.js +4 -4
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +21 -21
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +204 -44
- package/dist/install/install.mjs +9 -21
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1337 -490
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +25 -2
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/dist/scripts/_lib/compile_time_toggles.js +4 -3
- package/dist/scripts/_lib/compile_time_toggles.js.map +1 -1
- package/dist/server/io/yamlIO.js +0 -2
- package/dist/server/io/yamlIO.js.map +1 -1
- package/dist/server/schemas/settings.js +3 -9
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/shared/settingsClasses.js +64 -2
- package/dist/shared/settingsClasses.js.map +1 -1
- package/dist/ui/assets/{index-B5qGYOJo.js → index-5fXOHoVh.js} +2 -2
- package/dist/ui/assets/index-5fXOHoVh.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/contracts/settings-classes.md +307 -145
- package/docs/guidelines/agent-infra/active-remediation-mechanics.md +54 -0
- package/docs/guidelines/agent-infra/layered-settings.md +1 -2
- package/docs/guidelines/agent-infra/symptom-driven-harvest-loop.md +104 -0
- package/docs/guidelines/design-antipatterns.md +79 -10
- package/docs/guidelines/design-fidelity-mechanics.md +120 -2
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +0 -1
- package/src/config/agent-settings.template.yml +12 -30
- package/src/config/gate-coverage.yml +42 -8
- package/src/config/gate-violation-baselines.json +15 -3
- package/src/config/profiles/balanced.ini +0 -2
- package/src/config/profiles/full.ini +1 -3
- package/src/config/profiles/minimal.ini +0 -2
- package/src/config/review-prompt-binding-baseline.json +19 -0
- package/src/config/rule-enforcement-baseline.json +0 -1
- package/src/scripts/_cli/cmd_session_recycle.ts +66 -9
- package/src/scripts/_cli/handoff_sessions.ts +3 -2
- package/src/scripts/_dispatch.bash +1 -0
- package/src/scripts/_lib/agent_settings.ts +25 -2
- package/src/scripts/_lib/bench_telegraph.ts +1 -1
- package/src/scripts/_lib/cc_transcript.ts +25 -0
- package/src/scripts/_lib/compile_time_toggles.ts +4 -3
- package/src/scripts/ai_council/clients.ts +243 -34
- package/src/scripts/cache_realization_report.ts +3 -2
- package/src/scripts/check_ci_local_parity.ts +60 -6
- package/src/scripts/check_review_prompt_binding.ts +553 -0
- package/src/scripts/check_rule_projection_integrity.ts +3 -1
- package/src/scripts/check_suppression_hygiene.ts +19 -3
- package/src/scripts/conformance_scan.ts +282 -9
- package/src/scripts/cost/track.mjs +10 -4
- package/src/scripts/council_cli.ts +33 -3
- package/src/scripts/design_slop_rules.test.ts +37 -6
- package/src/scripts/design_slop_rules.ts +150 -22
- package/src/scripts/dispatch_r2_reviewer.ts +15 -1
- package/src/scripts/external_sources_denylist.json +3 -1
- package/src/scripts/hook_manifest.yaml +17 -6
- package/src/scripts/hooks/block_unauthorized_git.ts +54 -8
- package/src/scripts/hooks/delegation_nudge_hook.ts +33 -8
- package/src/scripts/hooks/evidence_independence.ts +61 -9
- package/src/scripts/hooks/session_eol_hook.ts +123 -3
- package/src/scripts/hooks/turn_end_gate_hook.ts +31 -0
- package/src/scripts/hooks/ui_route_nudge_hook.ts +55 -2
- package/src/scripts/lint_design_antipattern_parity.ts +201 -0
- package/src/scripts/lint_harvest_provenance.ts +483 -0
- package/src/scripts/lint_settings_classes.ts +273 -11
- package/src/scripts/lint_skill_router_head.ts +254 -0
- package/src/scripts/lint_symptom_intake.ts +241 -0
- package/src/scripts/report_consultation_rate.ts +128 -2
- package/src/scripts/report_skill_activation.ts +6 -4
- package/src/scripts/report_skill_obligation_violations.ts +2 -1
- package/src/scripts/run_skill_evals.ts +10 -5
- package/src/scripts/schemas/persona.schema.json +8 -0
- package/src/scripts/schemas/provenance-harvest.schema.json +39 -0
- package/src/scripts/sweep_dead_scan_roots.ts +51 -0
- package/src/server/io/yamlIO.ts +0 -2
- package/src/server/schemas/settings.ts +3 -19
- package/src/shared/settingsClasses.ts +93 -2
- package/dist/ui/assets/index-B5qGYOJo.js.map +0 -1
|
@@ -6,17 +6,31 @@
|
|
|
6
6
|
* The scope constraint is the point, and it is a council ruling
|
|
7
7
|
* (2026-08-06): *"a conformance scan that checks un-mechanised rules is
|
|
8
8
|
* theatre — if you can't gate it, don't pretend measuring it post-hoc is
|
|
9
|
-
* enforcement."* So
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* the gate disagree silently.
|
|
9
|
+
* enforcement."* So every classifier is IMPORTED from the gate it measures
|
|
10
|
+
* rather than re-implemented here. A second copy of a classifier is the "second
|
|
11
|
+
* artefact to keep in sync" the repo's own principle forbids, and it would let
|
|
12
|
+
* the scan and the gate disagree silently.
|
|
14
13
|
*
|
|
15
14
|
* language-pin ← language_mirror_hook.classify
|
|
16
15
|
* git-authorization ← git_authorization_hook.classifyAuthorization
|
|
17
16
|
* + block_unauthorized_git.commandOp / BLOCK_OPS
|
|
18
17
|
* vacuous-evidence ← before_complete_hook.isVacuousOutput / isCiPoll / pendingCount
|
|
19
18
|
* evidence-steering ← evidence_independence.isEvaluationPrompt / preloadedVerdict
|
|
19
|
+
* completion-claim ← turn_end_gate_hook.detectCompletionClaim (round 7)
|
|
20
|
+
* task-completeness ← delegation_nudge_hook.enumeratedFileTokens /
|
|
21
|
+
* FILE_SIGNAL_FLOOR (below)
|
|
22
|
+
*
|
|
23
|
+
* The count was "exactly four checks, one per shipped gate" and both halves have
|
|
24
|
+
* since moved, so both are stated plainly rather than left to rot: there are
|
|
25
|
+
* SIX, and `task-completeness` is the one with NO gate behind it. That is not a
|
|
26
|
+
* quiet exception to the council ruling — it is the ruling's own logic run
|
|
27
|
+
* forwards. The ruling forbids presenting a post-hoc measurement AS enforcement;
|
|
28
|
+
* this check exists to decide whether a refusal is warranted at all, before one
|
|
29
|
+
* is built, because the alternative is a gate resting on an unmeasured premise.
|
|
30
|
+
* The pre-registered bar it will be read against is committed BEFORE the number
|
|
31
|
+
* (`road-to-completion-loop` Phase 2), and a published null — no gate — is an
|
|
32
|
+
* accepted outcome. Nothing here claims the check enforces anything: this file
|
|
33
|
+
* is a REPORT, as its exit contract below says.
|
|
20
34
|
*
|
|
21
35
|
* Deliberately NOT scanned: ask-shape, session-canary, promissory closings,
|
|
22
36
|
* checkbox batching, symptom-vs-root-cause. Those are left as prose by
|
|
@@ -34,6 +48,7 @@ import path from "node:path";
|
|
|
34
48
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
35
49
|
|
|
36
50
|
import { censusRuleDir } from "./_lib/carrier_divergence.js";
|
|
51
|
+
import { projectStoreSlug } from "./_lib/cc_transcript.js";
|
|
37
52
|
import { entryText, isSidechain } from "./_lib/transcript_entry.js";
|
|
38
53
|
|
|
39
54
|
import { classify } from "./language_mirror_hook.js";
|
|
@@ -45,6 +60,10 @@ import { isVacuousOutput, isCiPoll, pendingCount } from "./before_complete_hook.
|
|
|
45
60
|
// than reimplemented, so the measured rate and the gate cannot disagree.
|
|
46
61
|
import { detectCompletionClaim } from "./hooks/turn_end_gate_hook.js";
|
|
47
62
|
import { isEvaluationPrompt, isSelfScoped, preloadedVerdict } from "./hooks/evidence_independence.js";
|
|
63
|
+
// Phase 1 of `road-to-completion-loop` — the SAME prompt-signal extraction the
|
|
64
|
+
// delegation nudge classifies on, so the measurement cannot see a different
|
|
65
|
+
// deliverable set than the classifier does.
|
|
66
|
+
import { enumeratedFileTokens, FILE_SIGNAL_FLOOR } from "./hooks/delegation_nudge_hook.js";
|
|
48
67
|
|
|
49
68
|
/**
|
|
50
69
|
* This module's own repo root, NOT `process.cwd()`.
|
|
@@ -65,7 +84,8 @@ export interface Violation {
|
|
|
65
84
|
| "git-authorization"
|
|
66
85
|
| "vacuous-evidence"
|
|
67
86
|
| "evidence-steering"
|
|
68
|
-
| "completion-claim"
|
|
87
|
+
| "completion-claim"
|
|
88
|
+
| "task-completeness";
|
|
69
89
|
session: string;
|
|
70
90
|
at: string;
|
|
71
91
|
detail: string;
|
|
@@ -79,6 +99,19 @@ export interface Violation {
|
|
|
79
99
|
*/
|
|
80
100
|
turns_since_prompt?: number;
|
|
81
101
|
compaction_since_prompt?: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* `task-completeness` only. The deliverable set's size, how many of it the
|
|
104
|
+
* reply window touched, and the tokens it never touched.
|
|
105
|
+
*
|
|
106
|
+
* Structured rather than prose-only because 1.3 requires HAND-VALIDATING every
|
|
107
|
+
* hit: a validator needs the exact missed tokens to open the window and decide
|
|
108
|
+
* whether the omission was real or one of the legitimate shapes (a blocking
|
|
109
|
+
* question, a hand-back, a user-fenced scope, an explicitly deferred item).
|
|
110
|
+
* A count alone cannot be checked, which is the failure this field prevents.
|
|
111
|
+
*/
|
|
112
|
+
enumerated?: number;
|
|
113
|
+
addressed?: number;
|
|
114
|
+
missed?: string[];
|
|
82
115
|
}
|
|
83
116
|
|
|
84
117
|
export interface SessionReport {
|
|
@@ -97,6 +130,18 @@ export interface SessionReport {
|
|
|
97
130
|
de_pin_turns: number;
|
|
98
131
|
/** First entry timestamp, so a reader can era-split without a second pass. */
|
|
99
132
|
first_at: string;
|
|
133
|
+
/**
|
|
134
|
+
* `task-completeness`'s DENOMINATOR: reply windows that met the file floor and
|
|
135
|
+
* contained real assistant work, i.e. the windows on which the check could fire
|
|
136
|
+
* at all.
|
|
137
|
+
*
|
|
138
|
+
* Counted here beside the numerator for the same reason `de_pin_turns` is — a
|
|
139
|
+
* rate whose two halves are derived from different populations is the defect
|
|
140
|
+
* round 7 § 6.2 found in the language figure, where the same 152 violations read
|
|
141
|
+
* 6.5 % over all turns and 9.2 % over the eligible ones. Without this a hit
|
|
142
|
+
* COUNT is all there is, and a count is not a rate.
|
|
143
|
+
*/
|
|
144
|
+
completeness_windows: number;
|
|
100
145
|
}
|
|
101
146
|
|
|
102
147
|
/**
|
|
@@ -177,6 +222,17 @@ export interface RateRecord {
|
|
|
177
222
|
de_pin_turns?: number;
|
|
178
223
|
/** The same numerator over `de_pin_turns`. Reported beside `rate_pct`, never instead. */
|
|
179
224
|
rate_pct_de_pin?: number;
|
|
225
|
+
/**
|
|
226
|
+
* `task-completeness`'s two halves, optional for the same archive reason as the
|
|
227
|
+
* four above: no line written before this check existed carries them.
|
|
228
|
+
*
|
|
229
|
+
* The rate is over WINDOWS, not turns or sessions — the only denominator on
|
|
230
|
+
* which the check can fire — and it is persisted beside the count so a later
|
|
231
|
+
* reader cannot mistake a hit count for a rate.
|
|
232
|
+
*/
|
|
233
|
+
completeness_windows?: number;
|
|
234
|
+
task_completeness?: number;
|
|
235
|
+
task_completeness_rate_pct?: number;
|
|
180
236
|
/** The corpus straddles a carrier landing, so no band verdict. */
|
|
181
237
|
era_spanning?: boolean;
|
|
182
238
|
/** Which carrier, when `era_spanning` — empty otherwise. */
|
|
@@ -265,6 +321,7 @@ export const CHECK_IDS: readonly CheckId[] = [
|
|
|
265
321
|
"vacuous-evidence",
|
|
266
322
|
"evidence-steering",
|
|
267
323
|
"completion-claim",
|
|
324
|
+
"task-completeness",
|
|
268
325
|
];
|
|
269
326
|
|
|
270
327
|
/**
|
|
@@ -297,6 +354,36 @@ export const CHECK_MEANINGS: Record<CheckId, string> = {
|
|
|
297
354
|
"each followed by the user handing the work back; this is the same " +
|
|
298
355
|
"predicate `turn-end-gate`'s completion detector refuses on, so the rate " +
|
|
299
356
|
"and the gate cannot disagree. A session that never polled CI can never hit.",
|
|
357
|
+
"task-completeness":
|
|
358
|
+
"A reply window that left at least one file the prompt ENUMERATED " +
|
|
359
|
+
"untouched — the token appears in neither any tool-call input nor any " +
|
|
360
|
+
"assistant prose before the next user prompt. Scope is deliberately narrow: " +
|
|
361
|
+
`only prompts naming >= ${FILE_SIGNAL_FLOOR} distinct file tokens are ` +
|
|
362
|
+
"measured, the same floor `delegation_nudge_hook` uses for its FILE shape, " +
|
|
363
|
+
"and the token list comes from that module rather than a second regex.\n" +
|
|
364
|
+
" What distinguishes a hit from a false read: a hit is EVIDENCE OF AN " +
|
|
365
|
+
"OMISSION, not proof of one. Four legitimate shapes produce the same " +
|
|
366
|
+
"signature and are NOT excluded here — a blocking question, a hand-back, a " +
|
|
367
|
+
"user-fenced scope, and an explicitly deferred item all address fewer files " +
|
|
368
|
+
"than the prompt named, correctly. That is why every hit is hand-validated " +
|
|
369
|
+
"and a precision is published beside the rate; an unvalidated count from " +
|
|
370
|
+
"this check means nothing.\n" +
|
|
371
|
+
" What it cannot see, stated so a zero is not misread as health: the " +
|
|
372
|
+
"ordered-plan, explicit-count, for-each and conjunction shapes name a " +
|
|
373
|
+
"COUNT of deliverables but not their identities, so this check is blind to " +
|
|
374
|
+
"them by construction — it does not score them complete, it does not score " +
|
|
375
|
+
"them at all. A prose deliverable that is not a filename is invisible to it.\n" +
|
|
376
|
+
" MEASURED PRECISION: 0 of 3 hits, over 4 eligible windows in 28 sessions " +
|
|
377
|
+
"(2026-08-12). All three were false positives from ONE extraction defect: the " +
|
|
378
|
+
"token list is taken from the whole prompt, so files quoted inside material " +
|
|
379
|
+
"the user PASTED — a review, a log, an example — read as deliverables. In all " +
|
|
380
|
+
"three, the user's own ask contributed zero tokens. `isInjectedBody` did not " +
|
|
381
|
+
"filter them because it only excludes long ENGLISH text, and this corpus is " +
|
|
382
|
+
"mostly German (12 295 and 7 656 chars, both classified `de`, both passed); " +
|
|
383
|
+
"the one English case missed the 2 500-char cut by 12. So a hit here is not " +
|
|
384
|
+
"evidence of anything until the ask is separated from the pasted material. " +
|
|
385
|
+
"Detector D was NOT built on this result — see " +
|
|
386
|
+
"`agents/evidence/analysis/task-completeness-measurement.md`.",
|
|
300
387
|
};
|
|
301
388
|
|
|
302
389
|
/**
|
|
@@ -333,6 +420,18 @@ export function renderWhy(report: ScanReport, id: CheckId): string {
|
|
|
333
420
|
`compaction_since_prompt=${compacted ?? "—"}`,
|
|
334
421
|
);
|
|
335
422
|
}
|
|
423
|
+
// `task-completeness` provenance. Printed as its own line because 1.3
|
|
424
|
+
// requires hand-validating every hit, and a validator needs the missed
|
|
425
|
+
// tokens verbatim — not a truncated detail string.
|
|
426
|
+
const missed = (v as { missed?: string[] }).missed;
|
|
427
|
+
if (missed !== undefined) {
|
|
428
|
+
const enumerated = (v as { enumerated?: number }).enumerated;
|
|
429
|
+
const addressed = (v as { addressed?: number }).addressed;
|
|
430
|
+
lines.push(
|
|
431
|
+
` provenance: enumerated=${enumerated ?? "—"} · ` +
|
|
432
|
+
`addressed=${addressed ?? "—"} · missed=[${missed.join(", ")}]`,
|
|
433
|
+
);
|
|
434
|
+
}
|
|
336
435
|
}
|
|
337
436
|
return lines.join("\n");
|
|
338
437
|
}
|
|
@@ -418,6 +517,54 @@ export const HARNESS_TEXT =
|
|
|
418
517
|
const EN_OPENER =
|
|
419
518
|
/^(let me|i'?ll |i'?m |found it|ok[,. ]|okay|alright|here'?s|now[,. ]|looking|checking|reading|running|the |this |that |there |we |you |good |right[,. ]|perfect|done[.,]|all |confirmed|correct)/i;
|
|
420
519
|
|
|
520
|
+
/**
|
|
521
|
+
* One prompt's reply window, for the `task-completeness` check.
|
|
522
|
+
*
|
|
523
|
+
* A "turn" in this store is one ENTRY, and a single reply spans several of them
|
|
524
|
+
* (one prose entry plus one per tool call). Scoring per entry would report every
|
|
525
|
+
* tool-call entry as incomplete, so the unit is the window from a counted user
|
|
526
|
+
* prompt to the next one — which is what step 1.1's "the turn's own user prompt"
|
|
527
|
+
* means in the transcript's actual shape. The departure is recorded rather than
|
|
528
|
+
* silently taken.
|
|
529
|
+
*/
|
|
530
|
+
export interface CompletenessWindow {
|
|
531
|
+
/** The PROMPT's timestamp, not an assistant entry's: it is where a validator opens the window. */
|
|
532
|
+
at: string;
|
|
533
|
+
tokens: string[];
|
|
534
|
+
addressed: Set<string>;
|
|
535
|
+
/**
|
|
536
|
+
* The window contained at least one assistant prose entry or tool call.
|
|
537
|
+
*
|
|
538
|
+
* An empty window is a truncated or abandoned session, not an omission — and
|
|
539
|
+
* counting it would make the rate a measure of how often a store ends
|
|
540
|
+
* mid-reply. That is the "detector reproduces the defect it measures" trap
|
|
541
|
+
* this check's own risk register names.
|
|
542
|
+
*/
|
|
543
|
+
worked: boolean;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** Mark every deliverable token this text mentions as addressed. */
|
|
547
|
+
export function markAddressed(text: string, w: CompletenessWindow): void {
|
|
548
|
+
if (w.tokens.length === 0) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const hay = text.toLowerCase();
|
|
552
|
+
for (const t of w.tokens) {
|
|
553
|
+
if (!w.addressed.has(t) && hay.includes(t)) {
|
|
554
|
+
w.addressed.add(t);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/** The window's verdict: the untouched tokens, or `null` for no finding. */
|
|
560
|
+
export function evaluateWindow(w: CompletenessWindow): { missed: string[] } | null {
|
|
561
|
+
if (!w.worked || w.tokens.length === 0) {
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
const missed = w.tokens.filter((t) => !w.addressed.has(t));
|
|
565
|
+
return missed.length > 0 ? { missed } : null;
|
|
566
|
+
}
|
|
567
|
+
|
|
421
568
|
export function scanSession(sessionId: string, lines: string[]): SessionReport {
|
|
422
569
|
const report: SessionReport = {
|
|
423
570
|
session: sessionId,
|
|
@@ -426,6 +573,7 @@ export function scanSession(sessionId: string, lines: string[]): SessionReport {
|
|
|
426
573
|
violations: [],
|
|
427
574
|
de_pin_turns: 0,
|
|
428
575
|
first_at: "",
|
|
576
|
+
completeness_windows: 0,
|
|
429
577
|
};
|
|
430
578
|
|
|
431
579
|
let pinned: "de" | "en" | null = null;
|
|
@@ -443,6 +591,44 @@ export function scanSession(sessionId: string, lines: string[]): SessionReport {
|
|
|
443
591
|
// measured failure is a completion claim in a LATER turn than the poll.
|
|
444
592
|
let ciSeen = false;
|
|
445
593
|
let ciSettled = false;
|
|
594
|
+
// Phase 1 of `road-to-completion-loop` — the open reply window, or null when
|
|
595
|
+
// the last prompt named no deliverable set worth scoring.
|
|
596
|
+
let openWindow: CompletenessWindow | null = null;
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Close the open window and record a finding if anything went untouched.
|
|
600
|
+
* Called on the NEXT counted prompt and once after the loop — a window left
|
|
601
|
+
* open at end-of-file is the most common shape in a live store and dropping it
|
|
602
|
+
* would silently bias the rate toward long sessions.
|
|
603
|
+
*/
|
|
604
|
+
const closeWindow = (): void => {
|
|
605
|
+
if (openWindow === null) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
// The denominator counts windows the check COULD fire on. A window with no
|
|
609
|
+
// assistant work is excluded from both halves — `evaluateWindow` already
|
|
610
|
+
// refuses to score it, and counting it below while never counting it above
|
|
611
|
+
// would deflate the rate by however often a store ends mid-reply.
|
|
612
|
+
if (openWindow.worked) {
|
|
613
|
+
report.completeness_windows += 1;
|
|
614
|
+
}
|
|
615
|
+
const verdict = evaluateWindow(openWindow);
|
|
616
|
+
if (verdict !== null) {
|
|
617
|
+
const addressed = openWindow.tokens.length - verdict.missed.length;
|
|
618
|
+
report.violations.push({
|
|
619
|
+
check: "task-completeness",
|
|
620
|
+
session: sessionId,
|
|
621
|
+
at: openWindow.at,
|
|
622
|
+
detail:
|
|
623
|
+
`prompt enumerated ${openWindow.tokens.length} file(s), reply window touched ` +
|
|
624
|
+
`${addressed}; untouched: ${verdict.missed.join(", ").slice(0, 160)}`,
|
|
625
|
+
enumerated: openWindow.tokens.length,
|
|
626
|
+
addressed,
|
|
627
|
+
missed: verdict.missed,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
openWindow = null;
|
|
631
|
+
};
|
|
446
632
|
|
|
447
633
|
for (const line of lines) {
|
|
448
634
|
if (!line.trim()) {
|
|
@@ -516,6 +702,21 @@ export function scanSession(sessionId: string, lines: string[]): SessionReport {
|
|
|
516
702
|
continue;
|
|
517
703
|
}
|
|
518
704
|
report.user_turns += 1;
|
|
705
|
+
// Phase 1 — the previous window ends HERE, at the next real chat message.
|
|
706
|
+
// Ordered deliberately before the new window opens: the filters above
|
|
707
|
+
// (`isCompactSummary`, `isSyntheticPrompt`, `isInjectedBody`) all `continue`
|
|
708
|
+
// without closing, which is correct — an injected skill body does not end
|
|
709
|
+
// the user's reply window.
|
|
710
|
+
closeWindow();
|
|
711
|
+
const deliverables = enumeratedFileTokens(ut);
|
|
712
|
+
if (deliverables.length >= FILE_SIGNAL_FLOOR) {
|
|
713
|
+
openWindow = {
|
|
714
|
+
at: String(entry["timestamp"] ?? ""),
|
|
715
|
+
tokens: deliverables,
|
|
716
|
+
addressed: new Set<string>(),
|
|
717
|
+
worked: false,
|
|
718
|
+
};
|
|
719
|
+
}
|
|
519
720
|
turnsSincePrompt = 0;
|
|
520
721
|
compactionSincePrompt = false;
|
|
521
722
|
const c = classify(ut);
|
|
@@ -538,6 +739,18 @@ export function scanSession(sessionId: string, lines: string[]): SessionReport {
|
|
|
538
739
|
if (prose !== null) {
|
|
539
740
|
report.assistant_turns += 1;
|
|
540
741
|
turnsSincePrompt += 1;
|
|
742
|
+
const firstLine = (prose.split("\n").find((l) => l.trim()) ?? "").trim();
|
|
743
|
+
// Phase 1 — prose is half the window's evidence (tool inputs are the other
|
|
744
|
+
// half, marked below). Placed before the language block for the same reason
|
|
745
|
+
// the completion-claim check is: that block `continue`s on a harness banner,
|
|
746
|
+
// and evidence collected after it would be skipped without saying so.
|
|
747
|
+
// A harness retry banner is not the assistant writing, so it marks nothing
|
|
748
|
+
// AND does not make the window count as worked — otherwise a session that
|
|
749
|
+
// died on a 529 would read as an omission.
|
|
750
|
+
if (openWindow !== null && !HARNESS_TEXT.test(firstLine)) {
|
|
751
|
+
openWindow.worked = true;
|
|
752
|
+
markAddressed(prose, openWindow);
|
|
753
|
+
}
|
|
541
754
|
// Round 7 § 6.2 — the denominator the language check can actually fire on.
|
|
542
755
|
// Counted here, beside the numerator, so the two cannot be derived from
|
|
543
756
|
// different populations later.
|
|
@@ -578,6 +791,15 @@ export function scanSession(sessionId: string, lines: string[]): SessionReport {
|
|
|
578
791
|
}
|
|
579
792
|
|
|
580
793
|
for (const tu of toolUses(entry)) {
|
|
794
|
+
// Phase 1 — the other half of the window's evidence. Serialised whole
|
|
795
|
+
// rather than field-picked: a deliverable can be named in `file_path`,
|
|
796
|
+
// `command`, `pattern`, `old_string` or a prompt, and enumerating the
|
|
797
|
+
// fields that count is how a reader of `file_path` alone would score a
|
|
798
|
+
// `grep`-then-`sed` edit as untouched.
|
|
799
|
+
if (openWindow !== null) {
|
|
800
|
+
openWindow.worked = true;
|
|
801
|
+
markAddressed(`${tu.name} ${JSON.stringify(tu.input)}`, openWindow);
|
|
802
|
+
}
|
|
581
803
|
const cmd =
|
|
582
804
|
typeof tu.input["command"] === "string" ? (tu.input["command"] as string) : null;
|
|
583
805
|
if (cmd !== null) {
|
|
@@ -628,6 +850,11 @@ export function scanSession(sessionId: string, lines: string[]): SessionReport {
|
|
|
628
850
|
}
|
|
629
851
|
}
|
|
630
852
|
|
|
853
|
+
// The last prompt's window never sees a following prompt, so it is closed
|
|
854
|
+
// here. Dropping it would bias the rate toward sessions that happen to end on
|
|
855
|
+
// a user turn.
|
|
856
|
+
closeWindow();
|
|
857
|
+
|
|
631
858
|
return report;
|
|
632
859
|
}
|
|
633
860
|
|
|
@@ -644,10 +871,27 @@ export const DEFAULT_RATE_SERIES = path.join(
|
|
|
644
871
|
"conformance-rates.jsonl",
|
|
645
872
|
);
|
|
646
873
|
|
|
647
|
-
/**
|
|
874
|
+
/**
|
|
875
|
+
* Default Claude Code transcript store for a project directory.
|
|
876
|
+
*
|
|
877
|
+
* Claude Code slugs every character outside `[A-Za-z0-9-]`, not just the
|
|
878
|
+
* separator and the dot: `/Users/x/.claude` → `-Users-x--claude`, and a
|
|
879
|
+
* worktree named `feat+turn-end-gate-always-on` → `feat-turn-end-gate-always-on`.
|
|
880
|
+
*
|
|
881
|
+
* The narrower `[/.]` this replaced is why the 2026-08-12 session audit could
|
|
882
|
+
* not scan the worktree it was running in: the `+` survived into the computed
|
|
883
|
+
* slug, no such directory exists, and the scan printed "no transcript store"
|
|
884
|
+
* and measured NOTHING. A measurement tool that silently reports an empty
|
|
885
|
+
* corpus for a whole class of paths is worse than one that errors, because the
|
|
886
|
+
* zero is indistinguishable from a clean result.
|
|
887
|
+
*
|
|
888
|
+
* The character class is generalised rather than extended by one `+` because
|
|
889
|
+
* every one of the 56 store names on this machine matches `^[A-Za-z0-9-]+$` —
|
|
890
|
+
* there is no observed counter-example of a preserved special character, and
|
|
891
|
+
* guessing which OTHER character is next would repeat this defect.
|
|
892
|
+
*/
|
|
648
893
|
export function defaultStore(projectDir: string): string {
|
|
649
|
-
|
|
650
|
-
const slug = projectDir.replace(/[/.]/g, "-");
|
|
894
|
+
const slug = projectStoreSlug(projectDir);
|
|
651
895
|
return path.join(process.env["HOME"] ?? "", ".claude", "projects", slug);
|
|
652
896
|
}
|
|
653
897
|
|
|
@@ -742,6 +986,16 @@ export function scanStore(store: string, limit: number): ScanReport {
|
|
|
742
986
|
const de_pin_turns = per_session.reduce((n, s) => n + s.de_pin_turns, 0);
|
|
743
987
|
const rate_pct_de_pin =
|
|
744
988
|
de_pin_turns === 0 ? 0 : Number(((100 * language_pin) / de_pin_turns).toFixed(1));
|
|
989
|
+
// `task-completeness` over its own denominator. Kept separate from the language
|
|
990
|
+
// rate on purpose: the two checks fire on different populations, and a single
|
|
991
|
+
// `rate_pct` covering both would be the pooled figure round 7 § 6.1 had to
|
|
992
|
+
// retract.
|
|
993
|
+
const completeness_windows = per_session.reduce((n, s) => n + s.completeness_windows, 0);
|
|
994
|
+
const task_completeness = totals["task-completeness"] ?? 0;
|
|
995
|
+
const task_completeness_rate_pct =
|
|
996
|
+
completeness_windows === 0
|
|
997
|
+
? 0
|
|
998
|
+
: Number(((100 * task_completeness) / completeness_windows).toFixed(1));
|
|
745
999
|
const era = spansCarrierChange(per_session.map((s) => s.first_at));
|
|
746
1000
|
const delivered = measureDelivered(
|
|
747
1001
|
path.join(REPO_ROOT, ".claude", "rules"),
|
|
@@ -764,6 +1018,9 @@ export function scanStore(store: string, limit: number): ScanReport {
|
|
|
764
1018
|
rate_pct,
|
|
765
1019
|
de_pin_turns,
|
|
766
1020
|
rate_pct_de_pin,
|
|
1021
|
+
completeness_windows,
|
|
1022
|
+
task_completeness,
|
|
1023
|
+
task_completeness_rate_pct,
|
|
767
1024
|
era_spanning: era.spans,
|
|
768
1025
|
era_reason: era.what,
|
|
769
1026
|
band: bandVerdict(rate_pct, assistant_turns, { spansCarrier: era.spans }),
|
|
@@ -848,6 +1105,22 @@ export function render(report: ScanReport): string {
|
|
|
848
1105
|
);
|
|
849
1106
|
lines.push(' is the comparable one and the second is the honest one. Both are printed.');
|
|
850
1107
|
}
|
|
1108
|
+
// `task-completeness`, on its own denominator. Optional-read for the same
|
|
1109
|
+
// archive reason as the two above: a `rate` block recorded before this check
|
|
1110
|
+
// existed carries neither field, and the series exists to be read back.
|
|
1111
|
+
const cWindows = report.rate.completeness_windows;
|
|
1112
|
+
const cRate = report.rate.task_completeness_rate_pct;
|
|
1113
|
+
if (typeof cWindows === "number" && typeof cRate === "number") {
|
|
1114
|
+
lines.push("");
|
|
1115
|
+
lines.push(
|
|
1116
|
+
` task-completeness ${cRate.toFixed(1)}% ` +
|
|
1117
|
+
`(${report.rate.task_completeness ?? 0} of ${cWindows} eligible reply windows)`,
|
|
1118
|
+
);
|
|
1119
|
+
lines.push(" A hit is EVIDENCE of an omission, never proof: a blocking question, a");
|
|
1120
|
+
lines.push(" hand-back, a fenced scope and a deferred item all produce this signature");
|
|
1121
|
+
lines.push(" correctly. Read the hand-validated precision beside this figure or not");
|
|
1122
|
+
lines.push(" at all — `--why task-completeness` lists every hit with its missed tokens.");
|
|
1123
|
+
}
|
|
851
1124
|
if (report.rate.band === "era-spanning") {
|
|
852
1125
|
lines.push(
|
|
853
1126
|
` Not compared: the corpus SPANS a carrier landing — ${report.rate.era_reason || 'see CARRIER_CHANGES'}.`,
|
|
@@ -116,10 +116,16 @@ function dedupKey(rec) {
|
|
|
116
116
|
return `${id} ${reqId}`;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
// Claude Code
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
|
|
119
|
+
// Claude Code slugs every character outside [A-Za-z0-9-], not just separators
|
|
120
|
+
// and dots: `/x/.claude/y` → `-x--claude-y`, and `feat+wt` → `feat-wt`.
|
|
121
|
+
//
|
|
122
|
+
// This is an inline copy of `_lib/cc_transcript.projectStoreSlug` — a module
|
|
123
|
+
// boundary, not an oversight: this file imports node builtins only. Keep the
|
|
124
|
+
// two in step. It was narrowed twice for the same reason (first `/` alone
|
|
125
|
+
// missed dotted segments; then `[/.]` missed a `+` worktree and silently
|
|
126
|
+
// resolved no project dir at all), which is why the class is now closed rather
|
|
127
|
+
// than extended one character per incident.
|
|
128
|
+
function encodeProjectPath(cwd) { return cwd.replace(/[^A-Za-z0-9-]/g, '-'); }
|
|
123
129
|
|
|
124
130
|
function findProjectDir(cwd) {
|
|
125
131
|
const c = join(PROJECTS_DIR, encodeProjectPath(cwd));
|
|
@@ -777,7 +777,17 @@ function _postRunQuorum(
|
|
|
777
777
|
* "attendance is telemetry, never a silent drop" applies to the estimate
|
|
778
778
|
* preview too, not only to a completed pass.
|
|
779
779
|
*/
|
|
780
|
-
function _format_quorum_line(q: QuorumResult): string {
|
|
780
|
+
function _format_quorum_line(q: QuorumResult, phase?: QuorumEventPhase): string {
|
|
781
|
+
// The phase tag is not decoration. A degraded run prints attendance TWICE —
|
|
782
|
+
// the pre-run reading before the estimate table, the post-run reading after
|
|
783
|
+
// the consult — and the two disagree by construction: `2/2 present …
|
|
784
|
+
// concluded` then `0/2 present … INCONCLUSIVE`. Same prefix, opposite
|
|
785
|
+
// content, and until 2026-08-12 neither line said which was which, so an
|
|
786
|
+
// operator skimming (or anything grepping `council:quorum ·`) could take the
|
|
787
|
+
// stale one. The event log had solved this from the start with an explicit
|
|
788
|
+
// `phase` field; stdout had not. Optional, because the estimate path prints
|
|
789
|
+
// exactly one line and a tag there would claim a distinction it does not make.
|
|
790
|
+
const tag = phase === undefined ? '' : `${phase === 'pre_run' ? 'before the run' : 'after the run'} · `;
|
|
781
791
|
const verdict = q.status === 'concluded' ? 'concluded' : 'INCONCLUSIVE — release gate holds';
|
|
782
792
|
// Round 7 § 5.3 — a degraded pass says so. `1/2 present, needed 1 —
|
|
783
793
|
// concluded` is literally true and reads as agreement; with a threshold of 1
|
|
@@ -786,7 +796,7 @@ function _format_quorum_line(q: QuorumResult): string {
|
|
|
786
796
|
// what was missing is the word that stops a reader inferring convergence.
|
|
787
797
|
const degraded =
|
|
788
798
|
q.present < q.total ? ` ⚠️ DEGRADED — ${String(q.total - q.present)} member(s) did not answer; this is not convergence.` : '';
|
|
789
|
-
return `council:quorum · ${q.present}/${q.total} present, needed ${q.threshold} — ${verdict}.${degraded}`;
|
|
799
|
+
return `council:quorum · ${tag}${q.present}/${q.total} present, needed ${q.threshold} — ${verdict}.${degraded}`;
|
|
790
800
|
}
|
|
791
801
|
|
|
792
802
|
function build_members(settings: Dict, opts: BuildMembersOptions = {}): ExternalAIClient[] {
|
|
@@ -2621,7 +2631,9 @@ function cmd_run(
|
|
|
2621
2631
|
_stdout(format_install_hints(skipped) + '\n');
|
|
2622
2632
|
}
|
|
2623
2633
|
if (quorum_out.result !== null) {
|
|
2624
|
-
|
|
2634
|
+
// Tagged, because `cmd_run` prints attendance a SECOND time after the
|
|
2635
|
+
// consult and on a degraded pass the two readings contradict each other.
|
|
2636
|
+
_stdout(_format_quorum_line(quorum_out.result, 'pre_run') + '\n');
|
|
2625
2637
|
}
|
|
2626
2638
|
_stdout(
|
|
2627
2639
|
format_estimate_table(billable, estimates, {
|
|
@@ -2777,6 +2789,19 @@ function cmd_run(
|
|
|
2777
2789
|
lens: question.mode,
|
|
2778
2790
|
invocation: String(_getattr(args, 'invocation', 'agent')),
|
|
2779
2791
|
});
|
|
2792
|
+
// The post-run reading also has to REACH THE OPERATOR, not just the event
|
|
2793
|
+
// log and the payload. Measured 2026-08-12: a pass where BOTH members
|
|
2794
|
+
// errored printed `2/2 present, needed 1 — concluded` — the pre-run banner
|
|
2795
|
+
// from the estimate block above — then `wrote …json`, and nothing else. The
|
|
2796
|
+
// payload said `{status: "inconclusive", present: 0}` and the absent list
|
|
2797
|
+
// named both, so the record was honest the whole time; the only place the
|
|
2798
|
+
// failure was invisible was the stream the operator actually reads. Printing
|
|
2799
|
+
// it unconditionally, rather than only when it differs from the pre-run
|
|
2800
|
+
// value, is deliberate: "attendance is telemetry, never a silent drop"
|
|
2801
|
+
// (`_format_quorum_line`) reads the same way for an unchanged reading, and a
|
|
2802
|
+
// conditional print would make the absence of a line mean two different
|
|
2803
|
+
// things.
|
|
2804
|
+
_stdout(_format_quorum_line(post_run.quorum, 'post_run') + '\n');
|
|
2780
2805
|
// Phase 4.1 — verdict → handoff envelope. Same tally `render()` computes
|
|
2781
2806
|
// internally for the Vote Tally block (mirrored here, not imported from
|
|
2782
2807
|
// there, because `render()` re-derives it from the SAVED payload on a
|
|
@@ -4024,5 +4049,10 @@ export {
|
|
|
4024
4049
|
_parse_siblings_overrides,
|
|
4025
4050
|
_synthesize_ai_council_block,
|
|
4026
4051
|
_postRunQuorum,
|
|
4052
|
+
// Exported for the phase-tag test only. The two-contradictory-banners shape
|
|
4053
|
+
// it guards cannot be produced through `cmd_run` with an injected roster
|
|
4054
|
+
// (the pre-run print is gated on `build_members` having populated
|
|
4055
|
+
// `quorum_out`), and constructing a real roster would spend money.
|
|
4056
|
+
_format_quorum_line,
|
|
4027
4057
|
CouncilDisabledError,
|
|
4028
4058
|
};
|
|
@@ -100,16 +100,11 @@ const FIXTURES: Record<string, { ext: string; positive: string; negative: string
|
|
|
100
100
|
negative:
|
|
101
101
|
'<h1>Get Started</h1><p>Deploys are marked ✅ when healthy.</p><td>✅</td><button>Track progress</button>',
|
|
102
102
|
},
|
|
103
|
-
"slop-lock-shape": {
|
|
103
|
+
"slop-v8-lock-shape": {
|
|
104
104
|
ext: "css",
|
|
105
105
|
positive: ".a{border-radius:4px}.b{border-radius:8px}.c{border-radius:12px}.d{border-radius:20px}",
|
|
106
106
|
negative: ".a{border-radius:8px}.b{border-radius:12px}",
|
|
107
107
|
},
|
|
108
|
-
"slop-lock-colour": {
|
|
109
|
-
ext: "css",
|
|
110
|
-
positive: ".a{color:#e11d48}.b{background:#2563eb}.c{border-color:#16a34a}",
|
|
111
|
-
negative: ".a{color:#111827}.b{background:#6b7280}.c{border-color:#2563eb}",
|
|
112
|
-
},
|
|
113
108
|
"slop-l9-section-monotony": {
|
|
114
109
|
ext: "html",
|
|
115
110
|
// 8 identical media+text sections → 1 distinct layout family (< 4)
|
|
@@ -129,6 +124,42 @@ const FIXTURES: Record<string, { ext: string; positive: string; negative: string
|
|
|
129
124
|
// only 2 consecutive → within cap
|
|
130
125
|
negative: '<section><div class="grid"><img src="x"><h2>A</h2><p>t</p></div></section>'.repeat(2),
|
|
131
126
|
},
|
|
127
|
+
"slop-v4-over-rounded": {
|
|
128
|
+
ext: "css",
|
|
129
|
+
// 24px radius on an 80px chip — mid-range rounding on a small surface
|
|
130
|
+
positive: ".chip{width:80px;border-radius:24px;background:#1f5f9c}",
|
|
131
|
+
// a deliberate pill is a shape decision, not over-rounding
|
|
132
|
+
negative: ".chip{width:80px;border-radius:9999px;background:#1f5f9c}",
|
|
133
|
+
},
|
|
134
|
+
"slop-c3-dark-glow": {
|
|
135
|
+
ext: "css",
|
|
136
|
+
// zero-offset saturated glow inside a declared dark surface
|
|
137
|
+
positive: ".dark .btn{background:#0b0f14;box-shadow:0 0 18px #22d3ee}",
|
|
138
|
+
// same dark surface, ordinary offset depth — not a glow
|
|
139
|
+
negative: ".dark .btn{background:#0b0f14;box-shadow:0 2px 8px #22d3ee}",
|
|
140
|
+
},
|
|
141
|
+
"slop-t9-uppercase-body": {
|
|
142
|
+
ext: "css",
|
|
143
|
+
positive: "article p{text-transform:uppercase;font-size:16px}",
|
|
144
|
+
// a short field label is the legitimate home of uppercase
|
|
145
|
+
negative: ".field-label{text-transform:uppercase;font-size:12px}",
|
|
146
|
+
},
|
|
147
|
+
"slop-t10-wide-tracking-body": {
|
|
148
|
+
ext: "css",
|
|
149
|
+
positive: "article p{letter-spacing:0.12em;font-size:16px}",
|
|
150
|
+
negative: "article p{letter-spacing:0.01em;font-size:16px}",
|
|
151
|
+
},
|
|
152
|
+
"slop-m1-bounce-easing": {
|
|
153
|
+
ext: "css",
|
|
154
|
+
// y1 = 1.56 overshoots the target and returns — the bounce signature
|
|
155
|
+
positive: ".card{transition:transform .3s cubic-bezier(.34,1.56,.64,1)}",
|
|
156
|
+
negative: ".card{transition:transform .3s cubic-bezier(.4,0,.2,1)}",
|
|
157
|
+
},
|
|
158
|
+
"slop-m3-img-hover-transform": {
|
|
159
|
+
ext: "css",
|
|
160
|
+
positive: ".tile:hover img{transform:scale(1.05)}",
|
|
161
|
+
negative: ".tile:hover .tile__title{color:#16202b}",
|
|
162
|
+
},
|
|
132
163
|
};
|
|
133
164
|
|
|
134
165
|
function findingsFor(ruleId: string, content: string, ext: string, ctx = NO_CTX) {
|