@event4u/agent-config 14.17.0 → 14.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +207 -77
  5. package/README.md +72 -150
  6. package/dist/agent-src/README.md +1 -1
  7. package/dist/agent-src/commands/agent-handoff.md +2 -2
  8. package/dist/agent-src/commands/pr/merge.md +35 -44
  9. package/dist/agent-src/commands/review/changes.md +19 -19
  10. package/dist/agent-src/commands/review.md +2 -2
  11. package/dist/agent-src/contexts/execution/host-capability-manifest.md +30 -3
  12. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  13. package/dist/agent-src/rules/git-history-discipline.md +2 -2
  14. package/dist/agent-src/rules/guidelines.md +1 -1
  15. package/dist/agent-src/rules/session-canary.md +2 -2
  16. package/dist/agent-src/rules/token-efficiency.md +1 -1
  17. package/dist/agent-src/skills/accessibility-auditor/SKILL.md +2 -0
  18. package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
  19. package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +27 -1
  20. package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
  21. package/dist/agent-src/skills/code-intelligence/SKILL.md +60 -12
  22. package/dist/agent-src/skills/complexity-first-planning/SKILL.md +27 -0
  23. package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
  24. package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
  25. package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
  26. package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
  27. package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
  28. package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
  29. package/dist/agent-src/skills/design-review/SKILL.md +3 -1
  30. package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
  31. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
  32. package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
  33. package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
  34. package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
  35. package/dist/agent-src/skills/feature-planning/SKILL.md +34 -0
  36. package/dist/agent-src/skills/git-workflow/SKILL.md +37 -12
  37. package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +176 -0
  38. package/dist/agent-src/skills/laravel-mail/SKILL.md +44 -0
  39. package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
  40. package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
  41. package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
  42. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
  43. package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
  44. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
  45. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +40 -0
  46. package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
  47. package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
  48. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +29 -2
  49. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +53 -8
  50. package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +3 -1
  51. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  52. package/dist/cli/commands/recordTriggerEval.js +2 -2
  53. package/dist/cli/main.js +1 -1
  54. package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-GZTMRYCN.js} +405 -341
  55. package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-IDHUQIXP.js} +283 -132
  56. package/dist/cli-delegate/cmd_adr_effective.js +318 -81
  57. package/dist/cli-delegate/cmd_conformance.js +749 -669
  58. package/dist/cli-delegate/cmd_doctor.js +1 -1
  59. package/dist/cli-delegate/cmd_export.js +1 -1
  60. package/dist/cli-delegate/cmd_handoff.js +1 -1
  61. package/dist/cli-delegate/cmd_prune.js +1 -1
  62. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  63. package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
  64. package/dist/cli-delegate/cmd_sync.js +1 -1
  65. package/dist/cli-delegate/cmd_uninstall.js +1 -1
  66. package/dist/cli-delegate/cmd_validate.js +1 -1
  67. package/dist/discovery/deprecation-report.md +1 -1
  68. package/dist/discovery/discovery-manifest.json +39 -38
  69. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  70. package/dist/discovery/discovery-manifest.summary.md +1 -1
  71. package/dist/discovery/dormancy-report.md +1 -1
  72. package/dist/discovery/orphan-report.md +1 -1
  73. package/dist/discovery/packs.json +2 -2
  74. package/dist/discovery/trust-report.md +1 -1
  75. package/dist/discovery/workspaces.json +2 -2
  76. package/dist/hooks/dispatch.js +2764 -2439
  77. package/dist/install/install.mjs +588 -527
  78. package/dist/mcp/registry-manifest.json +2 -2
  79. package/dist/mcp/server.json +2 -2
  80. package/dist/mcp/server.mjs +1381 -1138
  81. package/dist/scripts/_lib/model_tier.js +1 -1
  82. package/dist/shared/settingsConsent.js +1 -1
  83. package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
  84. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
  85. package/docs/guidelines/design-antipatterns.md +87 -64
  86. package/package.json +1 -1
  87. package/src/agent-src/README.md +1 -1
  88. package/src/config/ci-local-parity.yml +6 -0
  89. package/src/config/conformance-claim-baseline.json +9 -0
  90. package/src/config/gate-coverage.yml +170 -0
  91. package/src/config/gate-violation-baselines.json +4 -4
  92. package/src/config/release-gate-locality.yml +141 -0
  93. package/src/scripts/_cli/cmd_doctor.ts +2 -2
  94. package/src/scripts/_cli/cmd_handoff.ts +1 -1
  95. package/src/scripts/_cli/cmd_update.ts +1 -1
  96. package/src/scripts/_cli/cmd_versions.ts +1 -1
  97. package/src/scripts/_lib/adr_frontmatter.ts +44 -0
  98. package/src/scripts/_lib/consequence_objects.ts +152 -0
  99. package/src/scripts/_lib/exec_evidence.ts +7 -0
  100. package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
  101. package/src/scripts/_lib/host_permission_checks.ts +251 -0
  102. package/src/scripts/_lib/model_tier.ts +1 -1
  103. package/src/scripts/_lib/obligation_frequency.ts +2 -2
  104. package/src/scripts/_lib/release_highlights.ts +265 -1
  105. package/src/scripts/_lib/release_material.ts +129 -0
  106. package/src/scripts/_lib/repeated_run.ts +77 -0
  107. package/src/scripts/_lib/runtime_journal.ts +26 -3
  108. package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
  109. package/src/scripts/_lib/session_register.ts +11 -6
  110. package/src/scripts/_lib/sqlite_schema.ts +22 -0
  111. package/src/scripts/_lib/surface.ts +86 -0
  112. package/src/scripts/_lib/tamper_vocabulary.ts +242 -0
  113. package/src/scripts/_lib/ui_authority.ts +116 -0
  114. package/src/scripts/_lib/unified_diff.ts +121 -0
  115. package/src/scripts/adr/regenerate_index.ts +54 -1
  116. package/src/scripts/adr_cite_check.ts +71 -15
  117. package/src/scripts/ai_council/clients.ts +1 -1
  118. package/src/scripts/autonomy_friction_corpus.ts +229 -0
  119. package/src/scripts/build_proof.ts +15 -2
  120. package/src/scripts/check_beta_review_markers.ts +141 -17
  121. package/src/scripts/check_enforcement_coverage.ts +1 -1
  122. package/src/scripts/check_enforcement_denominator.ts +121 -11
  123. package/src/scripts/check_finding_dispositions.ts +239 -6
  124. package/src/scripts/check_gate_coverage.ts +96 -0
  125. package/src/scripts/check_installed_hooks_fresh.ts +381 -0
  126. package/src/scripts/check_release_highlights.ts +99 -14
  127. package/src/scripts/check_skill_activation_claim.ts +179 -0
  128. package/src/scripts/check_suppression_hygiene.ts +11 -0
  129. package/src/scripts/check_trigger_evals.ts +1 -1
  130. package/src/scripts/code_graph/build.ts +230 -33
  131. package/src/scripts/code_graph/extract.ts +168 -33
  132. package/src/scripts/code_graph/types.ts +37 -2
  133. package/src/scripts/code_graph/validate.ts +12 -1
  134. package/src/scripts/conformance_scan.ts +45 -4
  135. package/src/scripts/detect_silent_catch.ts +403 -0
  136. package/src/scripts/detect_verification_tampering.ts +538 -0
  137. package/src/scripts/explain_run.ts +1 -1
  138. package/src/scripts/generate_capability_matrix.ts +1 -1
  139. package/src/scripts/git_authorization_hook.ts +228 -18
  140. package/src/scripts/hook_manifest.json +1 -1
  141. package/src/scripts/hook_manifest.yaml +47 -30
  142. package/src/scripts/hooks/augment-dispatcher.sh +1 -1
  143. package/src/scripts/hooks/block_no_verify.ts +3 -3
  144. package/src/scripts/hooks/category_a.ts +291 -0
  145. package/src/scripts/hooks/cline-dispatcher.sh +1 -1
  146. package/src/scripts/hooks/concern_registry.ts +2 -2
  147. package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
  148. package/src/scripts/hooks/dispatch_hook.ts +17 -22
  149. package/src/scripts/hooks/evidence_independence.ts +21 -2
  150. package/src/scripts/hooks/{block_unauthorized_git.ts → git_command_classifier.ts} +175 -364
  151. package/src/scripts/hooks/host_lowering.ts +215 -0
  152. package/src/scripts/hooks/host_lowering.yaml +185 -0
  153. package/src/scripts/hooks/host_semantics.ts +105 -37
  154. package/src/scripts/hooks/journal_record_hook.ts +10 -0
  155. package/src/scripts/hooks/permission_gate.ts +117 -0
  156. package/src/scripts/hooks/push_settle_hook.ts +250 -0
  157. package/src/scripts/hooks/ship_diff_volume_hook.ts +1 -1
  158. package/src/scripts/hooks_status.ts +12 -10
  159. package/src/scripts/install-hooks.sh +134 -1
  160. package/src/scripts/install.ts +53 -82
  161. package/src/scripts/install_anthropic_key.sh +2 -2
  162. package/src/scripts/language_mirror_hook.ts +27 -5
  163. package/src/scripts/lint_design_antipattern_parity.ts +80 -0
  164. package/src/scripts/lint_evidence_artifacts.ts +45 -1
  165. package/src/scripts/lint_hook_manifest.ts +87 -3
  166. package/src/scripts/lint_motion_authority_drift.ts +731 -0
  167. package/src/scripts/lint_readme_size.ts +158 -12
  168. package/src/scripts/lint_reasoning_fixtures.ts +216 -0
  169. package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
  170. package/src/scripts/measure_sibling_naming.ts +150 -0
  171. package/src/scripts/redact_hook_capture.ts +1 -1
  172. package/src/scripts/release.ts +31 -143
  173. package/src/scripts/release_drill.ts +71 -1
  174. package/src/scripts/release_publication.ts +243 -3
  175. package/src/scripts/release_verify.ts +220 -0
  176. package/src/scripts/report_skill_activation.ts +319 -7
  177. package/src/scripts/schemas/review-findings.schema.json +6 -1
  178. package/src/scripts/schemas/skill.schema.json +12 -2
  179. package/src/scripts/self_review_gate.ts +10 -1
  180. package/src/scripts/session_canary_hook.ts +1 -1
  181. package/src/scripts/session_register_hook.ts +4 -3
  182. package/src/scripts/trigger_eval_grandfather.json +5 -1
  183. package/src/scripts/validate_agent_settings.ts +1 -1
  184. package/src/shared/settingsConsent.ts +1 -1
@@ -0,0 +1,242 @@
1
+ /**
2
+ * The closed vocabulary the verification-tampering detector speaks.
3
+ *
4
+ * Declared in its own module, and declared BEFORE the detector, because the
5
+ * roadmap step that asks for it (`road-to-deterministic-defect-detectors` 1.1)
6
+ * asks for exactly that ordering: a detector that invents its ids while it is
7
+ * being written ends up with the ids its implementation found easy, not the
8
+ * ids the failure classes need. Every id below has a fixture in
9
+ * `tests/fixtures/tamper-corpus/`, committed one commit earlier.
10
+ *
11
+ * WHY THE OUTCOME TYPE EXTENDS `GateOutcome` INSTEAD OF REPLACING IT.
12
+ *
13
+ * `_lib/gate_result.ts` already separates "the check ran and found something"
14
+ * (`violations`) from "the check could not run" (`crashed`) from "the
15
+ * measurement is void" (`estate_invalid`). That distinction is the one a tamper
16
+ * detector most needs — a detector that crashed reporting `clean` is precisely
17
+ * the phantom green it exists to catch — so it is reused verbatim rather than
18
+ * re-spelled.
19
+ *
20
+ * Exactly ONE member is added, and only because `GateOutcome` cannot express
21
+ * it: `stale_verdict`. A runner that exited 0 before the last edit produces a
22
+ * green exit code over a tree that no longer exists. `clean` would launder it;
23
+ * `violations` would claim a finding nobody made; `crashed` would claim the
24
+ * runner failed when it succeeded — at the wrong time. The state is "it ran,
25
+ * it passed, and it is not evidence", and none of the four existing members
26
+ * says that. Adding a fifth member is a smaller change than a second
27
+ * vocabulary, which is what 1.1 forbids.
28
+ *
29
+ * SEVERITY, AND THE ONE CONTESTED CALL.
30
+ *
31
+ * Six ids are `block`. `verification-skipped` is `warn`, and that is not a
32
+ * confidence statement about the detection — an added `it.skip` is as
33
+ * mechanically certain as an added `.only`. It is a statement about the
34
+ * ACT: the source this roadmap came from is split on whether adding a skip
35
+ * on the way to green is tampering or triage, and this repository does not
36
+ * promote a class to blocking on a split. It ships `warn` with the corpus as
37
+ * the evidence a later promotion would need.
38
+ *
39
+ * THE ESCAPE HATCH IS SCOPED TO ONE ID, DELIBERATELY.
40
+ *
41
+ * A blanket `tamper-allow` in a commit message would suppress every detector at
42
+ * once, which turns one honest exception into a general amnesty. The grammar
43
+ * therefore names the id: `tamper-allow: <detector-id> — <reason>`. The reason
44
+ * floor mirrors `check_gate_completeness.MIN_REASON_CHARS` (20) and exists for
45
+ * the same reason stated there — it rejects `n/a`, and it cannot judge whether
46
+ * a reason is true, which stays a human read.
47
+ */
48
+ import type { GateOutcome } from './gate_result.js';
49
+
50
+ /** Every tampering class the detector can name. Closed set. */
51
+ export type TamperDetectorId =
52
+ | 'assertion-removed'
53
+ | 'assertion-weakened'
54
+ | 'verification-skipped'
55
+ | 'expectation-rewritten'
56
+ | 'early-exit-injected'
57
+ | 'test-file-deleted'
58
+ | 'subject-mocked-away';
59
+
60
+ export type TamperSeverity = 'block' | 'warn';
61
+
62
+ export interface TamperDetectorSpec {
63
+ id: TamperDetectorId;
64
+ severity: TamperSeverity;
65
+ /** What shape in the diff fires this id. */
66
+ what: string;
67
+ /** Why that shape is a weakened verification rather than ordinary work. */
68
+ why: string;
69
+ }
70
+
71
+ export const TAMPER_DETECTORS: readonly TamperDetectorSpec[] = [
72
+ {
73
+ id: 'assertion-removed',
74
+ severity: 'block',
75
+ what: 'a removed assertion line with no assertion added in the same hunk',
76
+ why: 'the check that could fail is gone; what remains passes for a wider set of outputs',
77
+ },
78
+ {
79
+ id: 'assertion-weakened',
80
+ severity: 'block',
81
+ what: 'a removed assertion replaced in the same hunk by one lower on the strength ladder',
82
+ why: 'an exact-value check became an existence check, which passes for the broken output too',
83
+ },
84
+ {
85
+ id: 'verification-skipped',
86
+ severity: 'warn',
87
+ what: 'an added skip / xfail / only / ignore marker on a test',
88
+ why: 'the assertion survives the diff and stops running, which is the same thing at verdict time',
89
+ },
90
+ {
91
+ id: 'expectation-rewritten',
92
+ severity: 'block',
93
+ what: 'the same assertion call on both sides of a hunk with a different literal expected value',
94
+ why: 'the expectation moved onto whatever the code now emits, so the test can no longer disagree with it',
95
+ },
96
+ {
97
+ id: 'early-exit-injected',
98
+ severity: 'block',
99
+ what: 'an added unconditional return or exit above surviving assertions in a test body',
100
+ why: 'the test reports green having executed none of the verification below it',
101
+ },
102
+ {
103
+ id: 'test-file-deleted',
104
+ severity: 'block',
105
+ what: 'a test file deleted outright by the diff',
106
+ why: 'the fastest route to a green suite is the one that verifies nothing',
107
+ },
108
+ {
109
+ id: 'subject-mocked-away',
110
+ severity: 'block',
111
+ what: 'an added mock or stub whose target also appears on a removed line of the same file',
112
+ why: 'the function under test is replaced by a stub, so the test now asserts against its own fixture',
113
+ },
114
+ ] as const;
115
+
116
+ /** Ids in declaration order — the canonical listing order for output. */
117
+ export const TAMPER_DETECTOR_IDS: readonly TamperDetectorId[] = TAMPER_DETECTORS.map((d) => d.id);
118
+
119
+ const BY_ID = new Map<string, TamperDetectorSpec>(TAMPER_DETECTORS.map((d) => [d.id, d]));
120
+
121
+ /** True when `id` is a member of the closed set. */
122
+ export function isTamperDetectorId(id: string): id is TamperDetectorId {
123
+ return BY_ID.has(id);
124
+ }
125
+
126
+ export function detectorSpec(id: TamperDetectorId): TamperDetectorSpec {
127
+ const spec = BY_ID.get(id);
128
+ /* c8 ignore next 3 -- unreachable while the map is built from the same array */
129
+ if (spec === undefined) {
130
+ throw new Error(`unknown tamper detector id: ${id}`);
131
+ }
132
+ return spec;
133
+ }
134
+
135
+ /**
136
+ * The loop's outcome vocabulary: `GateOutcome` plus the one state it cannot say.
137
+ *
138
+ * See the module header for why this is an extension of four members rather
139
+ * than a parallel set of five.
140
+ */
141
+ export type LoopOutcome = GateOutcome | 'stale_verdict';
142
+
143
+ /**
144
+ * One finding. `file` and `line` are mandatory and are checked at runtime by
145
+ * {@link assertWellFormed} — a finding without a location is a claim a reader
146
+ * cannot check, which is the shape of report this tree already refuses.
147
+ */
148
+ export interface TamperFinding {
149
+ id: TamperDetectorId;
150
+ severity: TamperSeverity;
151
+ /** Repo-relative path as it appears in the diff header. */
152
+ file: string;
153
+ /** 1-based line number in the side of the diff the evidence lives on. */
154
+ line: number;
155
+ /** Which side `line` indexes — a removal has no post-image line. */
156
+ side: 'added' | 'removed';
157
+ /** The offending line, trimmed. Never empty. */
158
+ evidence: string;
159
+ }
160
+
161
+ /**
162
+ * Reject a finding that cannot be located.
163
+ *
164
+ * Deliberately a throw and not a filter: a detector that silently drops its own
165
+ * malformed findings under-reports, and under-reporting is the failure mode the
166
+ * whole detector exists to close.
167
+ */
168
+ export function assertWellFormed(finding: TamperFinding): TamperFinding {
169
+ if (finding.file.trim() === '') {
170
+ throw new Error(`tamper finding ${finding.id} carries no file`);
171
+ }
172
+ if (!Number.isInteger(finding.line) || finding.line < 1) {
173
+ throw new Error(
174
+ `tamper finding ${finding.id} in ${finding.file} carries no usable line ` +
175
+ `(got ${String(finding.line)}); every finding must be locatable as file:line`,
176
+ );
177
+ }
178
+ if (finding.evidence.trim() === '') {
179
+ throw new Error(`tamper finding ${finding.id} at ${finding.file}:${String(finding.line)} carries no evidence text`);
180
+ }
181
+ return finding;
182
+ }
183
+
184
+ /** `path/to/file.ts:12` — the one location format the detector prints. */
185
+ export function locate(finding: TamperFinding): string {
186
+ return `${finding.file}:${String(finding.line)}`;
187
+ }
188
+
189
+ /**
190
+ * Grammar of the scoped escape hatch, read from a commit message.
191
+ *
192
+ * Both an em dash and a plain hyphen are accepted as the separator: the
193
+ * repository writes em dashes in prose, and a contributor typing this into
194
+ * `git commit` will reach for `-`. Rejecting the hyphen would make the hatch
195
+ * fail in the direction that hides a legitimate change.
196
+ */
197
+ export const TAMPER_ALLOW_RE = /^\s*tamper-allow:\s*([a-z-]+)\s*(?:—|--|-)\s*(.+)$/gim;
198
+
199
+ /** Mirrors `check_gate_completeness.MIN_REASON_CHARS` — rejects `n/a`, judges nothing. */
200
+ export const MIN_ALLOW_REASON_CHARS = 20;
201
+
202
+ export interface AllowDeclaration {
203
+ id: TamperDetectorId;
204
+ reason: string;
205
+ }
206
+
207
+ export interface AllowParse {
208
+ allowed: Set<TamperDetectorId>;
209
+ /** Declarations that named an unknown id or gave too short a reason. */
210
+ malformed: string[];
211
+ }
212
+
213
+ /**
214
+ * Parse every `tamper-allow:` line out of a commit message.
215
+ *
216
+ * A malformed declaration does NOT suppress anything and is reported instead:
217
+ * a typo'd id that silently allowed nothing would read to its author as an
218
+ * accepted exception, and a typo'd id that silently allowed everything would be
219
+ * the amnesty this grammar exists to prevent.
220
+ */
221
+ export function parseAllowDeclarations(message: string): AllowParse {
222
+ const allowed = new Set<TamperDetectorId>();
223
+ const malformed: string[] = [];
224
+ TAMPER_ALLOW_RE.lastIndex = 0;
225
+ for (const match of message.matchAll(TAMPER_ALLOW_RE)) {
226
+ const rawId = (match[1] ?? '').trim();
227
+ const reason = (match[2] ?? '').trim();
228
+ if (!isTamperDetectorId(rawId)) {
229
+ malformed.push(`tamper-allow names an unknown detector id: "${rawId}"`);
230
+ continue;
231
+ }
232
+ if (reason.length < MIN_ALLOW_REASON_CHARS) {
233
+ malformed.push(
234
+ `tamper-allow for "${rawId}" gives a ${String(reason.length)}-character reason; ` +
235
+ `at least ${String(MIN_ALLOW_REASON_CHARS)} are required`,
236
+ );
237
+ continue;
238
+ }
239
+ allowed.add(rawId);
240
+ }
241
+ return { allowed, malformed };
242
+ }
@@ -341,3 +341,119 @@ export function operationConflicts(authority: UiAuthority, op: Operation): Confl
341
341
  }
342
342
  return out;
343
343
  }
344
+
345
+ /* -------------------------------------------------------------------------- */
346
+ /* Antipattern signal classes against the precedence list */
347
+ /* -------------------------------------------------------------------------- */
348
+
349
+ /**
350
+ * The five classes a `design-antipatterns.md` catalog row may claim.
351
+ *
352
+ * Kept here, beside the precedence list, rather than in the catalog gate: the
353
+ * question "may this signal overrule what the user supplied" is a precedence
354
+ * question, and precedence lives in exactly one module by construction.
355
+ */
356
+ export type SignalClass =
357
+ | 'floor'
358
+ | 'invariant'
359
+ | 'craft-presumption'
360
+ | 'style-preference'
361
+ | 'reference-constraint';
362
+
363
+ /** The two classes that are opinions rather than thresholds. */
364
+ export const SOFT_CLASSES = ['craft-presumption', 'style-preference'] as const;
365
+
366
+ export interface SignalVerdict {
367
+ /**
368
+ * `blocks` — refuse the surface. `flags` — surface it as a rebuttable
369
+ * presumption. `yields` — say nothing: something with more authority has
370
+ * already decided this.
371
+ */
372
+ outcome: 'blocks' | 'flags' | 'yields';
373
+ /** The precedence level that outranked it, numbered as in this file's header. */
374
+ outranked_by: { level: number; source: ProvenanceSource; detail: string } | null;
375
+ reason: string;
376
+ }
377
+
378
+ /**
379
+ * Where a soft signal sits against the precedence list.
380
+ *
381
+ * A craft presumption is a good default and nothing more. When the run has a
382
+ * supplied reference (4), a coherent incumbent (5), or a project `DESIGN.md`
383
+ * register (6), that artifact has already decided the question the presumption
384
+ * would re-open, and re-opening it is the redesign-by-taste failure
385
+ * `design-fidelity` exists to stop.
386
+ *
387
+ * A `floor` or an `invariant` is NOT a taste call — an accessibility threshold
388
+ * or a structural correctness rule — so nothing on the list releases it, and a
389
+ * reference artifact that violates one is a finding about the reference.
390
+ *
391
+ * LEVELS 1–3 ARE DELIBERATELY NOT MECHANISED HERE. Explicit user authority, a
392
+ * registered hard constraint, and the surface brief all outrank a presumption a
393
+ * fortiori, but `Signals` carries them only as whole-surface fields
394
+ * (`surface_mode`, `register`, `change_intent`) with no per-pattern statement.
395
+ * Reading a brief's mere presence as blanket permission for every antipattern
396
+ * would silence the catalog on every briefed surface, which is the opposite of
397
+ * what those levels mean. A per-pattern override is `DESIGN.md`'s declared
398
+ * suppression, which is level 6 and IS handled.
399
+ */
400
+ export function resolveSignal(signalClass: SignalClass, signals: Signals = {}): SignalVerdict {
401
+ if (signalClass === 'floor' || signalClass === 'invariant') {
402
+ return {
403
+ outcome: 'blocks',
404
+ outranked_by: null,
405
+ reason:
406
+ `a ${signalClass} is a threshold, not a preference — no supplied artifact releases it, `
407
+ + 'and an artifact that violates one is a finding about the artifact',
408
+ };
409
+ }
410
+
411
+ if (signalClass === 'reference-constraint') {
412
+ const present = signals.reference !== undefined || signals.design_md?.present === true;
413
+ return present
414
+ ? {
415
+ outcome: 'blocks',
416
+ outranked_by: null,
417
+ reason: 'the reference or DESIGN.md this constraint is bound to is present in this run, so it carries that artifact\'s own authority',
418
+ }
419
+ : {
420
+ outcome: 'flags',
421
+ outranked_by: null,
422
+ reason: 'the reference this constraint is bound to is not present in this run, so it has no artifact behind it',
423
+ };
424
+ }
425
+
426
+ if (signals.reference !== undefined) {
427
+ return {
428
+ outcome: 'yields',
429
+ outranked_by: { level: 4, source: 'reference-artifact', detail: signals.reference.path ?? signals.reference.maturity },
430
+ reason: `a ${signalClass} never outranks a supplied reference artifact`,
431
+ };
432
+ }
433
+ if (signals.incumbent?.coherent === true) {
434
+ return {
435
+ outcome: 'yields',
436
+ outranked_by: { level: 5, source: 'incumbent-scan', detail: 'coherent incumbent' },
437
+ reason: `a ${signalClass} never outranks a coherent incumbent`,
438
+ };
439
+ }
440
+ if (signals.design_md?.present === true) {
441
+ return {
442
+ outcome: 'yields',
443
+ outranked_by: { level: 6, source: 'design-md', detail: signals.design_md.register ?? 'register unstated' },
444
+ reason: `a ${signalClass} never outranks a project DESIGN.md register`,
445
+ };
446
+ }
447
+ if (signals.product_md?.present === true) {
448
+ return {
449
+ outcome: 'yields',
450
+ outranked_by: { level: 6, source: 'product-md', detail: signals.product_md.register ?? 'register unstated' },
451
+ reason: `a ${signalClass} never outranks a project PRODUCT.md register`,
452
+ };
453
+ }
454
+ return {
455
+ outcome: 'flags',
456
+ outranked_by: null,
457
+ reason: `a ${signalClass} with nothing above it on the precedence list is a rebuttable presumption`,
458
+ };
459
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * A minimal unified-diff reader.
3
+ *
4
+ * Written rather than pulled in: nothing in `src/scripts/_lib/` parses a diff
5
+ * (`grep -rl "diff --git" src/scripts/_lib/*.ts` returns nothing), and the two
6
+ * consumers here need three facts a full patch library would surround with
7
+ * fifty more — which side a line is on, what line number it lands on, and
8
+ * whether the file was deleted outright.
9
+ *
10
+ * Deliberately tolerant in one direction only: an unparseable line is skipped
11
+ * rather than thrown on, because a diff carrying an unexpected header must not
12
+ * make the detector crash into a green. Deliberately strict in the other: a
13
+ * hunk header that does not parse aborts the file, because line numbers that
14
+ * are wrong are worse than absent — every finding this feeds is a `file:line`
15
+ * claim a reader will try to open.
16
+ */
17
+
18
+ export interface DiffLine {
19
+ side: 'added' | 'removed' | 'context';
20
+ /** 1-based line number on the side this line belongs to. */
21
+ line: number;
22
+ /** Line content with the leading +/-/space removed. */
23
+ text: string;
24
+ }
25
+
26
+ export interface DiffHunk {
27
+ header: string;
28
+ lines: DiffLine[];
29
+ }
30
+
31
+ export interface DiffFile {
32
+ /** Path as it appears after `b/`, or after `a/` for a deleted file. */
33
+ path: string;
34
+ /** True when the diff removes the file outright. */
35
+ deleted: boolean;
36
+ /** True when the diff creates the file. */
37
+ added: boolean;
38
+ hunks: DiffHunk[];
39
+ }
40
+
41
+ const HUNK_RE = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
42
+ const GIT_HEADER_RE = /^diff --git a\/(.+?) b\/(.+)$/;
43
+
44
+ /** Parse a unified diff into files and hunks. Never throws on content. */
45
+ export function parseUnifiedDiff(text: string): DiffFile[] {
46
+ const files: DiffFile[] = [];
47
+ let current: DiffFile | null = null;
48
+ let hunk: DiffHunk | null = null;
49
+ let oldNo = 0;
50
+ let newNo = 0;
51
+
52
+ for (const raw of text.split('\n')) {
53
+ const gitHeader = GIT_HEADER_RE.exec(raw);
54
+ if (gitHeader !== null) {
55
+ current = { path: gitHeader[2] ?? '', deleted: false, added: false, hunks: [] };
56
+ files.push(current);
57
+ hunk = null;
58
+ continue;
59
+ }
60
+ if (current === null) continue;
61
+
62
+ if (raw.startsWith('deleted file mode')) {
63
+ current.deleted = true;
64
+ continue;
65
+ }
66
+ if (raw.startsWith('new file mode')) {
67
+ current.added = true;
68
+ continue;
69
+ }
70
+ if (raw.startsWith('+++ ')) {
71
+ // `+++ /dev/null` is the other spelling of a deletion; some
72
+ // producers emit it without the `deleted file mode` line.
73
+ if (raw.slice(4).trim() === '/dev/null') current.deleted = true;
74
+ continue;
75
+ }
76
+ if (raw.startsWith('--- ')) {
77
+ if (raw.slice(4).trim() === '/dev/null') current.added = true;
78
+ continue;
79
+ }
80
+ if (raw.startsWith('index ') || raw.startsWith('similarity index') || raw.startsWith('rename ')) {
81
+ continue;
82
+ }
83
+
84
+ const hunkHeader = HUNK_RE.exec(raw);
85
+ if (hunkHeader !== null) {
86
+ oldNo = Number(hunkHeader[1]);
87
+ newNo = Number(hunkHeader[3]);
88
+ hunk = { header: raw, lines: [] };
89
+ current.hunks.push(hunk);
90
+ continue;
91
+ }
92
+ if (hunk === null) continue;
93
+
94
+ if (raw.startsWith('+')) {
95
+ hunk.lines.push({ side: 'added', line: newNo, text: raw.slice(1) });
96
+ newNo += 1;
97
+ } else if (raw.startsWith('-')) {
98
+ hunk.lines.push({ side: 'removed', line: oldNo, text: raw.slice(1) });
99
+ oldNo += 1;
100
+ } else if (raw.startsWith(' ') || raw === '') {
101
+ hunk.lines.push({ side: 'context', line: newNo, text: raw.slice(1) });
102
+ oldNo += 1;
103
+ newNo += 1;
104
+ }
105
+ // `` and anything else: skipped on purpose.
106
+ }
107
+ return files;
108
+ }
109
+
110
+ /** Every added line across the whole diff, with its file. */
111
+ export function addedLines(files: readonly DiffFile[]): Array<{ file: DiffFile; line: DiffLine }> {
112
+ const out: Array<{ file: DiffFile; line: DiffLine }> = [];
113
+ for (const file of files) {
114
+ for (const h of file.hunks) {
115
+ for (const l of h.lines) {
116
+ if (l.side === 'added') out.push({ file, line: l });
117
+ }
118
+ }
119
+ }
120
+ return out;
121
+ }
@@ -16,7 +16,7 @@ import * as fs from 'node:fs';
16
16
  import * as path from 'node:path';
17
17
  import process from 'node:process';
18
18
  import { fileURLToPath, pathToFileURL } from 'node:url';
19
- import { readAdrAxisCells, readAdrFrontmatterScalars } from '../_lib/adr_frontmatter.js';
19
+ import { dated_trigger_day, readAdrAxisCells, readAdrFrontmatterScalars } from '../_lib/adr_frontmatter.js';
20
20
  import { generatedByItalic } from '../_lib/generated_by.js';
21
21
 
22
22
  // ^ADR-(\d{3})-([a-z0-9-]+)\.md$
@@ -262,6 +262,41 @@ export function scanOutside(repoRoot: string, roots: readonly string[] = OUTSIDE
262
262
  return out;
263
263
  }
264
264
 
265
+ /**
266
+ * The rows whose `review_trigger` opens with a calendar date.
267
+ *
268
+ * WHY THIS SECTION CARRIES THE DATE AND NOT THE FIRED/NOT-FIRED STATE
269
+ * ------------------------------------------------------------------
270
+ * `regenerate_index --check` byte-compares this file in CI
271
+ * (`.github/workflows/rule-backstops.yml`), so anything rendered here has to be
272
+ * a function of the tree alone. A `fired` / `not-fired` column would be a
273
+ * function of the tree AND the day, which means the index would go stale on a
274
+ * calendar boundary nobody committed and red every open pull request until
275
+ * someone regenerated it. The date is the decidable half; the reader compares
276
+ * it to today, and `adr_cite_check` prints the comparison for a machine.
277
+ */
278
+ export interface DatedTriggerRow {
279
+ label: string;
280
+ path: string;
281
+ day: string;
282
+ decision: string;
283
+ }
284
+
285
+ export function datedTriggerRows(rows: readonly Row[]): DatedTriggerRow[] {
286
+ const out: DatedTriggerRow[] = [];
287
+ for (const r of rows) {
288
+ const day = dated_trigger_day(r.review_trigger ?? '');
289
+ if (day === null) continue;
290
+ out.push({
291
+ label: 'num' in r ? `ADR-${r.num as string}` : (r.path as string).slice(0, -3),
292
+ path: r.path as string,
293
+ day,
294
+ decision: r.decision ?? r.slug ?? '—',
295
+ });
296
+ }
297
+ return out.sort((a, b) => a.day.localeCompare(b.day) || a.label.localeCompare(b.label));
298
+ }
299
+
265
300
  /** Mirror `render(num, leg)`. */
266
301
  export function render(num: Row[], leg: Row[], outside: OutsideRow[] = []): string {
267
302
  const out = [
@@ -277,6 +312,24 @@ export function render(num: Row[], leg: Row[], outside: OutsideRow[] = []): stri
277
312
  if (leg.length > 0) {
278
313
  out.push('', '## Unnumbered (legacy)', '', HEAD, ...leg.map(row));
279
314
  }
315
+ const dated = datedTriggerRows([...num, ...leg]);
316
+ if (dated.length > 0) {
317
+ out.push(
318
+ '',
319
+ '## Dated review triggers',
320
+ '',
321
+ 'A `review_trigger` that opens with a calendar date is the one reopen condition',
322
+ 'this repository can decide without interpreting anything, so it is listed where a',
323
+ 'reader meets it rather than only in a command they would have to know to run.',
324
+ 'Compare the expiry to today; `./scripts-run src/scripts/adr_cite_check <ADR-NNN>`',
325
+ 'prints `fired` or `not-fired` for the same date. Every other trigger is semantic',
326
+ 'and reports `indeterminate` — a trigger that merely mentions a date is not listed.',
327
+ '',
328
+ '| Expiry | # | Decision |',
329
+ '|---|---|---|',
330
+ ...dated.map((d) => `| ${d.day} | [${d.label}](${d.path}) | ${d.decision} |`),
331
+ );
332
+ }
280
333
  if (outside.length > 0) {
281
334
  out.push(
282
335
  '',