@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.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +207 -77
- package/README.md +72 -150
- package/dist/agent-src/README.md +1 -1
- package/dist/agent-src/commands/agent-handoff.md +2 -2
- package/dist/agent-src/commands/pr/merge.md +35 -44
- package/dist/agent-src/commands/review/changes.md +19 -19
- package/dist/agent-src/commands/review.md +2 -2
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +30 -3
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/git-history-discipline.md +2 -2
- package/dist/agent-src/rules/guidelines.md +1 -1
- package/dist/agent-src/rules/session-canary.md +2 -2
- package/dist/agent-src/rules/token-efficiency.md +1 -1
- package/dist/agent-src/skills/accessibility-auditor/SKILL.md +2 -0
- package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
- package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +27 -1
- package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
- package/dist/agent-src/skills/code-intelligence/SKILL.md +60 -12
- package/dist/agent-src/skills/complexity-first-planning/SKILL.md +27 -0
- package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
- package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
- package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
- package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
- package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
- package/dist/agent-src/skills/design-review/SKILL.md +3 -1
- package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
- package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
- package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
- package/dist/agent-src/skills/feature-planning/SKILL.md +34 -0
- package/dist/agent-src/skills/git-workflow/SKILL.md +37 -12
- package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +176 -0
- package/dist/agent-src/skills/laravel-mail/SKILL.md +44 -0
- package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
- package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
- package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
- package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
- package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +40 -0
- package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +29 -2
- package/dist/agent-src/skills/verify-repair-loop/SKILL.md +53 -8
- package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +3 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/commands/recordTriggerEval.js +2 -2
- package/dist/cli/main.js +1 -1
- package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-GZTMRYCN.js} +405 -341
- package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-IDHUQIXP.js} +283 -132
- package/dist/cli-delegate/cmd_adr_effective.js +318 -81
- package/dist/cli-delegate/cmd_conformance.js +749 -669
- package/dist/cli-delegate/cmd_doctor.js +1 -1
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +39 -38
- 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 +2764 -2439
- package/dist/install/install.mjs +588 -527
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1381 -1138
- package/dist/scripts/_lib/model_tier.js +1 -1
- package/dist/shared/settingsConsent.js +1 -1
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
- package/docs/guidelines/design-antipatterns.md +87 -64
- package/package.json +1 -1
- package/src/agent-src/README.md +1 -1
- package/src/config/ci-local-parity.yml +6 -0
- package/src/config/conformance-claim-baseline.json +9 -0
- package/src/config/gate-coverage.yml +170 -0
- package/src/config/gate-violation-baselines.json +4 -4
- package/src/config/release-gate-locality.yml +141 -0
- package/src/scripts/_cli/cmd_doctor.ts +2 -2
- package/src/scripts/_cli/cmd_handoff.ts +1 -1
- package/src/scripts/_cli/cmd_update.ts +1 -1
- package/src/scripts/_cli/cmd_versions.ts +1 -1
- package/src/scripts/_lib/adr_frontmatter.ts +44 -0
- package/src/scripts/_lib/consequence_objects.ts +152 -0
- package/src/scripts/_lib/exec_evidence.ts +7 -0
- package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
- package/src/scripts/_lib/host_permission_checks.ts +251 -0
- package/src/scripts/_lib/model_tier.ts +1 -1
- package/src/scripts/_lib/obligation_frequency.ts +2 -2
- package/src/scripts/_lib/release_highlights.ts +265 -1
- package/src/scripts/_lib/release_material.ts +129 -0
- package/src/scripts/_lib/repeated_run.ts +77 -0
- package/src/scripts/_lib/runtime_journal.ts +26 -3
- package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
- package/src/scripts/_lib/session_register.ts +11 -6
- package/src/scripts/_lib/sqlite_schema.ts +22 -0
- package/src/scripts/_lib/surface.ts +86 -0
- package/src/scripts/_lib/tamper_vocabulary.ts +242 -0
- package/src/scripts/_lib/ui_authority.ts +116 -0
- package/src/scripts/_lib/unified_diff.ts +121 -0
- package/src/scripts/adr/regenerate_index.ts +54 -1
- package/src/scripts/adr_cite_check.ts +71 -15
- package/src/scripts/ai_council/clients.ts +1 -1
- package/src/scripts/autonomy_friction_corpus.ts +229 -0
- package/src/scripts/build_proof.ts +15 -2
- package/src/scripts/check_beta_review_markers.ts +141 -17
- package/src/scripts/check_enforcement_coverage.ts +1 -1
- package/src/scripts/check_enforcement_denominator.ts +121 -11
- package/src/scripts/check_finding_dispositions.ts +239 -6
- package/src/scripts/check_gate_coverage.ts +96 -0
- package/src/scripts/check_installed_hooks_fresh.ts +381 -0
- package/src/scripts/check_release_highlights.ts +99 -14
- package/src/scripts/check_skill_activation_claim.ts +179 -0
- package/src/scripts/check_suppression_hygiene.ts +11 -0
- package/src/scripts/check_trigger_evals.ts +1 -1
- package/src/scripts/code_graph/build.ts +230 -33
- package/src/scripts/code_graph/extract.ts +168 -33
- package/src/scripts/code_graph/types.ts +37 -2
- package/src/scripts/code_graph/validate.ts +12 -1
- package/src/scripts/conformance_scan.ts +45 -4
- package/src/scripts/detect_silent_catch.ts +403 -0
- package/src/scripts/detect_verification_tampering.ts +538 -0
- package/src/scripts/explain_run.ts +1 -1
- package/src/scripts/generate_capability_matrix.ts +1 -1
- package/src/scripts/git_authorization_hook.ts +228 -18
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +47 -30
- package/src/scripts/hooks/augment-dispatcher.sh +1 -1
- package/src/scripts/hooks/block_no_verify.ts +3 -3
- package/src/scripts/hooks/category_a.ts +291 -0
- package/src/scripts/hooks/cline-dispatcher.sh +1 -1
- package/src/scripts/hooks/concern_registry.ts +2 -2
- package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
- package/src/scripts/hooks/dispatch_hook.ts +17 -22
- package/src/scripts/hooks/evidence_independence.ts +21 -2
- package/src/scripts/hooks/{block_unauthorized_git.ts → git_command_classifier.ts} +175 -364
- package/src/scripts/hooks/host_lowering.ts +215 -0
- package/src/scripts/hooks/host_lowering.yaml +185 -0
- package/src/scripts/hooks/host_semantics.ts +105 -37
- package/src/scripts/hooks/journal_record_hook.ts +10 -0
- package/src/scripts/hooks/permission_gate.ts +117 -0
- package/src/scripts/hooks/push_settle_hook.ts +250 -0
- package/src/scripts/hooks/ship_diff_volume_hook.ts +1 -1
- package/src/scripts/hooks_status.ts +12 -10
- package/src/scripts/install-hooks.sh +134 -1
- package/src/scripts/install.ts +53 -82
- package/src/scripts/install_anthropic_key.sh +2 -2
- package/src/scripts/language_mirror_hook.ts +27 -5
- package/src/scripts/lint_design_antipattern_parity.ts +80 -0
- package/src/scripts/lint_evidence_artifacts.ts +45 -1
- package/src/scripts/lint_hook_manifest.ts +87 -3
- package/src/scripts/lint_motion_authority_drift.ts +731 -0
- package/src/scripts/lint_readme_size.ts +158 -12
- package/src/scripts/lint_reasoning_fixtures.ts +216 -0
- package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
- package/src/scripts/measure_sibling_naming.ts +150 -0
- package/src/scripts/redact_hook_capture.ts +1 -1
- package/src/scripts/release.ts +31 -143
- package/src/scripts/release_drill.ts +71 -1
- package/src/scripts/release_publication.ts +243 -3
- package/src/scripts/release_verify.ts +220 -0
- package/src/scripts/report_skill_activation.ts +319 -7
- package/src/scripts/schemas/review-findings.schema.json +6 -1
- package/src/scripts/schemas/skill.schema.json +12 -2
- package/src/scripts/self_review_gate.ts +10 -1
- package/src/scripts/session_canary_hook.ts +1 -1
- package/src/scripts/session_register_hook.ts +4 -3
- package/src/scripts/trigger_eval_grandfather.json +5 -1
- package/src/scripts/validate_agent_settings.ts +1 -1
- 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
|
'',
|