@event4u/agent-config 16.1.0 → 16.2.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 +237 -248
- package/dist/agent-src/commands/roadmap/process-full.md +1 -1
- package/dist/agent-src/commands/work.md +4 -0
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +4 -8
- package/dist/agent-src/rules/ui-audit-gate.md +1 -2
- package/dist/agent-src/skills/accessibility-auditor/data/aria-patterns.csv +1 -1
- package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +2 -2
- package/dist/agent-src/skills/design-review/SKILL.md +30 -4
- package/dist/agent-src/skills/design-review/references/verification-automation.md +32 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +16 -1
- package/dist/agent-src/skills/existing-ui-audit/references/component-taxonomy-detection.md +56 -0
- package/dist/agent-src/skills/markitdown/SKILL.md +4 -0
- package/dist/agent-src/skills/playwright-testing/SKILL.md +30 -6
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +23 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +30 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
- package/dist/agent-src/templates/scripts/work_engine/state.ts +57 -7
- package/dist/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
- package/dist/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
- package/dist/cli/python/knowledge_ingest.js +1 -1
- package/dist/cli/python/workspace_drive.js +1 -1
- package/dist/cli-delegate/{chunk-S3VFXMTM.js → chunk-4XFP2Y3E.js} +1 -1
- package/dist/cli-delegate/{chunk-HDVVGPQW.js → chunk-7XGBX6ZH.js} +471 -387
- package/dist/cli-delegate/{chunk-GJCBB2UG.js → chunk-FP36SWJD.js} +432 -144
- package/dist/cli-delegate/{chunk-UNCRJBT3.js → chunk-IGD6ERX6.js} +31 -0
- package/dist/cli-delegate/{chunk-FXYUYU4N.js → chunk-JNMVLWLR.js} +42 -1
- package/dist/cli-delegate/{chunk-QJWAUMJW.js → chunk-YJ72UIAK.js} +730 -15
- package/dist/cli-delegate/cmd_conformance.js +902 -782
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +4 -4
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +10 -10
- 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 +6666 -2581
- package/dist/hooks/dispatch.sha256 +1 -0
- package/dist/install/install.mjs +848 -733
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +2747 -1510
- package/dist/server/schemas/settings.js +23 -0
- package/dist/ui/assets/index-Cwq4d0hX.js +23 -0
- package/dist/ui/assets/index-Cyjy8bWO.css +1 -0
- package/dist/ui/index.html +2 -2
- package/docs/contracts/settings-classes.md +6 -4
- package/package.json +4 -4
- package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
- package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
- package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
- package/src/agent-src/templates/scripts/work_engine/state.ts +57 -7
- package/src/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
- package/src/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
- package/src/config/agent-settings.template.yml +29 -0
- package/src/config/ci-local-parity.yml +18 -0
- package/src/config/continuity-surface.json +2 -2
- package/src/config/gate-coverage.yml +234 -3
- package/src/config/gate-violation-baselines.json +7 -3
- package/src/config/hook-latency-budget.json +21 -0
- package/src/config/hook-token-budget.json +2 -2
- package/src/config/metric-registry.yml +114 -0
- package/src/config/placeholder-drift-budget.json +10 -0
- package/src/config/routing-coverage-seed.json +18 -4
- package/src/scripts/_cli/cmd_doctor.ts +5 -19
- package/src/scripts/_cli/doctor_execution.ts +100 -17
- package/src/scripts/_cli/doctor_network_posture.ts +226 -0
- package/src/scripts/_lib/body_portable.ts +102 -0
- package/src/scripts/_lib/cc_transcript.ts +132 -1
- package/src/scripts/_lib/concern_sla_window.ts +200 -0
- package/src/scripts/_lib/forge_protection.ts +114 -0
- package/src/scripts/_lib/forge_reader.ts +558 -0
- package/src/scripts/_lib/host_env_write.ts +111 -0
- package/src/scripts/_lib/llm_proposer_transport.ts +14 -4
- package/src/scripts/_lib/obligations.ts +119 -8
- package/src/scripts/_lib/probe_inputs.ts +271 -0
- package/src/scripts/_lib/read_surface_scan.ts +322 -0
- package/src/scripts/_lib/release_highlights.ts +2 -1
- package/src/scripts/_lib/retrieval_sanitize.ts +77 -16
- package/src/scripts/_lib/shell_write_shapes.ts +108 -0
- package/src/scripts/_lib/structural_hiding.ts +622 -0
- package/src/scripts/_lib/turn_end_refusals.ts +525 -4
- package/src/scripts/_lib/turn_end_transcript.ts +216 -0
- package/src/scripts/_lib/verification_command.ts +324 -0
- package/src/scripts/_lib/verification_evidence.ts +647 -0
- package/src/scripts/_lib/verify_clause.ts +280 -0
- package/src/scripts/ai_team/team_dispatch.ts +4 -0
- package/src/scripts/before_complete_hook.ts +340 -23
- package/src/scripts/bench_hook_latency.ts +412 -5
- package/src/scripts/cache_realization_report.ts +61 -1
- package/src/scripts/check_argument_hint.ts +287 -0
- package/src/scripts/check_claims.ts +125 -1
- package/src/scripts/check_enforcement_coverage.ts +38 -9
- package/src/scripts/check_enforcement_matrix.ts +245 -3
- package/src/scripts/check_finding_dispositions.ts +22 -3
- package/src/scripts/check_generator_sync.ts +71 -0
- package/src/scripts/check_host_docs_digest.ts +786 -0
- package/src/scripts/check_host_format_column.ts +559 -0
- package/src/scripts/check_input_references.ts +228 -0
- package/src/scripts/check_inputs_parity.ts +235 -0
- package/src/scripts/check_kernel_edit_ratified.ts +47 -5
- package/src/scripts/check_kill_switch_table.ts +294 -0
- package/src/scripts/check_placeholder_drift.ts +221 -0
- package/src/scripts/check_read_surface_coverage.ts +515 -0
- package/src/scripts/check_release_holds.ts +2 -1
- package/src/scripts/check_routing_coverage.ts +378 -11
- package/src/scripts/check_static_parity.ts +17 -0
- package/src/scripts/ci_settle.ts +145 -19
- package/src/scripts/closure_scan.ts +115 -1
- package/src/scripts/compile_hook_manifest.ts +60 -16
- package/src/scripts/external_sources_denylist.json +29 -4
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +42 -5
- package/src/scripts/hooks/block_config_weakening.ts +229 -4
- package/src/scripts/hooks/block_kernel_rule_writes.ts +9 -78
- package/src/scripts/hooks/block_plumbing_writes.ts +314 -0
- package/src/scripts/hooks/bundle_integrity.ts +291 -0
- package/src/scripts/hooks/chain_nudge_hook.ts +1 -2
- package/src/scripts/hooks/code_graph_context_hook.ts +1 -2
- package/src/scripts/hooks/comment_discipline_hook.ts +1 -3
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/concern_timings.ts +94 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +1 -2
- package/src/scripts/hooks/design_pass_hook.ts +100 -9
- package/src/scripts/hooks/design_slop_hook.ts +1 -2
- package/src/scripts/hooks/dispatch_hook.ts +25 -19
- package/src/scripts/hooks/edit_shape_hook.ts +1 -2
- package/src/scripts/hooks/end_review_nudge_hook.ts +1 -1
- package/src/scripts/hooks/evidence_independence.ts +1 -2
- package/src/scripts/hooks/exit_codes.ts +128 -0
- package/src/scripts/hooks/host_lowering.json +1 -0
- package/src/scripts/hooks/host_lowering.ts +128 -3
- package/src/scripts/hooks/host_lowering.yaml +262 -46
- package/src/scripts/hooks/host_semantics.ts +30 -2
- package/src/scripts/hooks/injection_budget.ts +9 -2
- package/src/scripts/hooks/interruption_ledger_hook.ts +1 -2
- package/src/scripts/hooks/journal_record_hook.ts +1 -2
- package/src/scripts/hooks/memory_recall_hook.ts +1 -2
- package/src/scripts/hooks/obligation_settle_hook.ts +151 -5
- package/src/scripts/hooks/one_question_per_ask_hook.ts +25 -4
- package/src/scripts/hooks/orchestration_record_hook.ts +1 -2
- package/src/scripts/hooks/permission_gate.ts +1 -2
- package/src/scripts/hooks/push_settle_hook.ts +1 -3
- package/src/scripts/hooks/replay_hook.ts +2 -1
- package/src/scripts/hooks/reread_guard_hook.ts +1 -2
- package/src/scripts/hooks/review_baseline_hook.ts +1 -2
- package/src/scripts/hooks/rtk_wrap_hook.ts +1 -2
- package/src/scripts/hooks/rule_inject_hook.ts +8 -5
- package/src/scripts/hooks/run_continuation_hook.ts +13 -15
- package/src/scripts/hooks/skill_route_hook.ts +1 -2
- package/src/scripts/hooks/source_first_gate_hook.ts +1 -2
- package/src/scripts/hooks/spawn_guard_shadow_hook.ts +1 -2
- package/src/scripts/hooks/subagent_ledger_hook.ts +1 -2
- package/src/scripts/hooks/suggestion_capture_hook.ts +1 -1
- package/src/scripts/hooks/table_fingerprint.ts +49 -0
- package/src/scripts/hooks/telemetry_flush_hook.ts +1 -2
- package/src/scripts/hooks/telemetry_usage_hook.ts +1 -2
- package/src/scripts/hooks/tool_result_bytes_hook.ts +93 -9
- package/src/scripts/hooks/turn_end_gate_hook.ts +337 -324
- package/src/scripts/hooks/ui_route_nudge_hook.ts +1 -2
- package/src/scripts/install.ts +3 -4
- package/src/scripts/lint_deny_text.ts +419 -0
- package/src/scripts/lint_exit_codes.ts +262 -0
- package/src/scripts/lint_hook_manifest.ts +55 -0
- package/src/scripts/lint_roadmap_blockers.ts +78 -12
- package/src/scripts/lint_skill_descriptions.ts +1 -1
- package/src/scripts/lint_skill_scripts_readonly.ts +1 -1
- package/src/scripts/mcp_server/prompts.ts +142 -1
- package/src/scripts/measure_skill_ranker_baseline.ts +305 -51
- package/src/scripts/measure_turn_end_gate.ts +193 -0
- package/src/scripts/memory_lookup.ts +1 -1
- package/src/scripts/prepack-check.mjs +53 -0
- package/src/scripts/release_publication.ts +13 -1
- package/src/scripts/report_invocation_surface.ts +383 -0
- package/src/scripts/report_obligation_carriers.ts +699 -0
- package/src/scripts/report_obligation_writer_split.ts +236 -0
- package/src/scripts/report_skill_menu_census.ts +208 -0
- package/src/scripts/report_standing_payload_by_host.ts +1 -1
- package/src/scripts/roadmap_verify_share.ts +213 -0
- package/src/scripts/schemas/command.schema.json +44 -0
- package/src/scripts/schemas/review-findings.schema.json +4 -0
- package/src/scripts/schemas/skill.schema.json +44 -0
- package/src/scripts/self_review_gate.ts +359 -10
- package/src/scripts/semantic_parity_census.ts +314 -0
- package/src/scripts/session_eol_report.ts +54 -1
- package/src/scripts/sweep_dead_scan_roots.ts +1 -1
- package/src/scripts/trigger_eval_grandfather.json +0 -2
- package/src/scripts/trigger_eval_rotation.ts +122 -27
- package/src/scripts/ui_conformance_probe.ts +156 -4
- package/src/scripts/update_prices.ts +85 -3
- package/src/scripts/write_bundle_digest.mjs +74 -0
- package/src/server/schemas/settings.ts +31 -0
- package/src/templates/consumer-settings/ONBOARDING.md +1 -0
- package/dist/ui/assets/index-DH1xc62v.js +0 -29
- package/dist/ui/assets/index-jSfMW8_t.css +0 -1
- /package/dist/cli-delegate/{chunk-YUM5KURG.js → chunk-7RQP4UDH.js} +0 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* check_read_surface_coverage.ts — generate the sanitize floor's read-surface
|
|
4
|
+
* table in `docs/contracts/retrieval-read-surfaces.md` from the tree, and fail
|
|
5
|
+
* when the committed table, the module header, or the structural detector
|
|
6
|
+
* disagree with what the tree actually says
|
|
7
|
+
* (road-to-a-sanitize-list-that-is-generated, Phase 1.2 + Phase 3.3).
|
|
8
|
+
*
|
|
9
|
+
* THREE CHECKS, ONE REASON
|
|
10
|
+
* ------------------------
|
|
11
|
+
* 1. DRIFT — the committed table equals the regenerated one. A new module that
|
|
12
|
+
* brings outside bytes in, or a sanitize import added or removed, changes the
|
|
13
|
+
* table; leaving the committed copy behind is the failure this gate exists
|
|
14
|
+
* for.
|
|
15
|
+
* 2. SECOND COPY — `retrieval_sanitize.ts`'s header points at the table instead
|
|
16
|
+
* of restating it. Two copies of a list is exactly how the first one went
|
|
17
|
+
* stale, twice, so a header that names a module path the table also names is
|
|
18
|
+
* a finding rather than a nicety.
|
|
19
|
+
* 3. NO PERCENTAGE — the structural-hiding detector publishes no recall or
|
|
20
|
+
* coverage rate. A rate needs a frozen corpus of hiding techniques and none
|
|
21
|
+
* exists; a number would be an invented denominator that a later reader
|
|
22
|
+
* quotes as measured coverage. Enforced rather than asserted, because a
|
|
23
|
+
* prose promise not to publish a number is the weakest control available.
|
|
24
|
+
*
|
|
25
|
+
* WHAT A GREEN RUN PROVES, AND WHAT IT DOES NOT
|
|
26
|
+
* ---------------------------------------------
|
|
27
|
+
* Green means the published table agrees with the tree's import graph. It
|
|
28
|
+
* proves nothing about whether any module applies the floor to the right
|
|
29
|
+
* string. `covered` is "the floor is in scope in this module"; establishing
|
|
30
|
+
* that it is actually reached needs a read of the module, and this gate does
|
|
31
|
+
* not pretend otherwise — the table header says so in its own first lines.
|
|
32
|
+
*/
|
|
33
|
+
import * as fs from 'node:fs';
|
|
34
|
+
import * as os from 'node:os';
|
|
35
|
+
import * as path from 'node:path';
|
|
36
|
+
import process from 'node:process';
|
|
37
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
38
|
+
|
|
39
|
+
import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
|
|
40
|
+
import {
|
|
41
|
+
LOCAL_VCS_EXCLUSION,
|
|
42
|
+
type ReadSurfaceRow,
|
|
43
|
+
SCAN_ROOT,
|
|
44
|
+
renderTable,
|
|
45
|
+
scanReadSurfaces,
|
|
46
|
+
tally,
|
|
47
|
+
} from './_lib/read_surface_scan.js';
|
|
48
|
+
import { DeadScopeError, reportScanned } from './_lib/scan_scope.js';
|
|
49
|
+
|
|
50
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
51
|
+
const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
52
|
+
|
|
53
|
+
const GATE = 'check_read_surface_coverage';
|
|
54
|
+
export const DOC_REL = 'docs/contracts/retrieval-read-surfaces.md';
|
|
55
|
+
export const FLOOR_REL = 'src/scripts/_lib/retrieval_sanitize.ts';
|
|
56
|
+
export const DETECTOR_REL = 'src/scripts/_lib/structural_hiding.ts';
|
|
57
|
+
|
|
58
|
+
export const BEGIN_MARKER = '<!-- BEGIN read-surface-table (generated) -->';
|
|
59
|
+
export const END_MARKER = '<!-- END read-surface-table -->';
|
|
60
|
+
|
|
61
|
+
const REGEN_COMMAND = `./scripts-run src/scripts/${GATE} --write`;
|
|
62
|
+
|
|
63
|
+
function _read(root: string, rel: string): string | null {
|
|
64
|
+
try {
|
|
65
|
+
return fs.readFileSync(path.join(root, rel), 'utf-8');
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The leading block comment of a module — its header, and nothing after it. */
|
|
72
|
+
export function leadingBlockComment(src: string): string {
|
|
73
|
+
const m = /^\s*\/\*\*[\s\S]*?\*\//.exec(src);
|
|
74
|
+
return m === null ? '' : m[0];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Render the full generated region, header prose included. */
|
|
78
|
+
export function renderRegion(rows: readonly ReadSurfaceRow[]): string {
|
|
79
|
+
return [
|
|
80
|
+
'> Generated by `' + GATE + '`. Do not edit between the markers — a cell',
|
|
81
|
+
'> edited by hand is overwritten by the next regeneration and reported by the',
|
|
82
|
+
'> gate in between. Corrections belong in the tree or in the scanner.',
|
|
83
|
+
'',
|
|
84
|
+
'**A row is a claim about imports, not about safety.** `covered` means the',
|
|
85
|
+
'module imports the sanitize floor, so the floor is in scope there. It does',
|
|
86
|
+
'not say the floor is applied to the right string or at the right point —',
|
|
87
|
+
'that needs a read of the module, and no import graph can supply it.',
|
|
88
|
+
'',
|
|
89
|
+
'**What is out of scope, said here rather than left to be discovered.** ' +
|
|
90
|
+
LOCAL_VCS_EXCLUSION,
|
|
91
|
+
'',
|
|
92
|
+
'A module reaching the network through a shape in neither the strong nor the',
|
|
93
|
+
'weak detector set is absent from this table entirely. That residual is the',
|
|
94
|
+
'reason `unclassified` exists and the reason this paragraph does.',
|
|
95
|
+
'',
|
|
96
|
+
renderTable(rows),
|
|
97
|
+
].join('\n');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Replace the marked region in `doc`, or append one when absent. */
|
|
101
|
+
export function spliceRegion(doc: string, region: string): string {
|
|
102
|
+
const begin = doc.indexOf(BEGIN_MARKER);
|
|
103
|
+
const end = doc.indexOf(END_MARKER);
|
|
104
|
+
if (begin === -1 || end === -1 || end < begin) {
|
|
105
|
+
return `${doc.trimEnd()}\n\n${BEGIN_MARKER}\n${region}\n${END_MARKER}\n`;
|
|
106
|
+
}
|
|
107
|
+
return `${doc.slice(0, begin)}${BEGIN_MARKER}\n${region}\n${doc.slice(end)}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Extract the marked region's body, or `null` when the markers are missing. */
|
|
111
|
+
export function extractRegion(doc: string): string | null {
|
|
112
|
+
const begin = doc.indexOf(BEGIN_MARKER);
|
|
113
|
+
const end = doc.indexOf(END_MARKER);
|
|
114
|
+
if (begin === -1 || end === -1 || end < begin) return null;
|
|
115
|
+
return doc.slice(begin + BEGIN_MARKER.length, end).trim();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Module paths the floor's header restates instead of pointing at.
|
|
120
|
+
*
|
|
121
|
+
* Matching on the basename keeps the check honest against a header that names
|
|
122
|
+
* `memory_lookup.ts` without its directory — the shape the previous header
|
|
123
|
+
* actually used.
|
|
124
|
+
*/
|
|
125
|
+
export function headerSecondCopy(header: string, rows: readonly ReadSurfaceRow[]): string[] {
|
|
126
|
+
const hits = new Set<string>();
|
|
127
|
+
for (const r of rows) {
|
|
128
|
+
const base = r.module.split('/').pop() ?? r.module;
|
|
129
|
+
if (header.includes(base)) hits.add(base);
|
|
130
|
+
}
|
|
131
|
+
return [...hits].sort();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
interface Options {
|
|
135
|
+
root: string;
|
|
136
|
+
write: boolean;
|
|
137
|
+
quiet: boolean;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* A published rate — `99 %`, `72.33%`, `recall: 0.98`.
|
|
142
|
+
*
|
|
143
|
+
* Scoped to the DETECTOR FILE and nothing else, deliberately. The codepoint
|
|
144
|
+
* floor next door publishes two real, corpus-backed percentages in its own
|
|
145
|
+
* docblock; a check that swept every citing surface would red on those and be
|
|
146
|
+
* switched off within a week. The claim this rule defends is narrow — the
|
|
147
|
+
* STRUCTURAL layer has no frozen corpus, so it has no denominator — and the
|
|
148
|
+
* check is scoped to exactly that claim.
|
|
149
|
+
*/
|
|
150
|
+
const PERCENTAGE_RE = /\b\d+(?:\.\d+)?\s*%|\brecall\s*[:=]\s*\d/i;
|
|
151
|
+
|
|
152
|
+
/** The register's exported name, cited by every surface claiming the coverage. */
|
|
153
|
+
export const GAP_REGISTER_SYMBOL = 'STRUCTURAL_HIDING_GAPS';
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* True when a percentage or recall figure is PUBLISHED in the detector.
|
|
157
|
+
*
|
|
158
|
+
* Inline code spans are removed before the test, and that exclusion is the
|
|
159
|
+
* difference between a rule and a nuisance. The gap register names
|
|
160
|
+
* `transform: translate(-100%, 0)` as an off-screen hiding technique — a CSS
|
|
161
|
+
* literal inside backticks, not a claim about this detector's coverage. The
|
|
162
|
+
* first version of this check red on exactly that and the fix belongs here,
|
|
163
|
+
* in the predicate, rather than in the register's wording: a gate that makes
|
|
164
|
+
* honest prose contort around it gets switched off. Both directions are pinned
|
|
165
|
+
* by self-test cases, because a widened matcher that no longer catches the
|
|
166
|
+
* thing it was written for fails silently.
|
|
167
|
+
*/
|
|
168
|
+
export function publishesRate(detectorSrc: string): boolean {
|
|
169
|
+
for (const line of detectorSrc.split('\n')) {
|
|
170
|
+
// The rule states the prohibition in prose; the prose is not a breach.
|
|
171
|
+
if (line.includes('PERCENTAGE_RE') || line.includes('no recall or coverage')) continue;
|
|
172
|
+
const prose = line.replace(/`[^`]*`/g, '');
|
|
173
|
+
if (PERCENTAGE_RE.test(prose)) return true;
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function parseArgs(argv: readonly string[]): Options {
|
|
179
|
+
const out: Options = { root: ROOT, write: false, quiet: false };
|
|
180
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
181
|
+
const a = argv[i];
|
|
182
|
+
if (a === '--root') {
|
|
183
|
+
out.root = path.resolve(argv[i + 1] ?? '.');
|
|
184
|
+
i += 1;
|
|
185
|
+
} else if (a === '--write') out.write = true;
|
|
186
|
+
else if (a === '--quiet') out.quiet = true;
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
192
|
+
if (argv.includes('--self-test')) return selfTest();
|
|
193
|
+
const opts = parseArgs(argv);
|
|
194
|
+
const say = (s: string): void => {
|
|
195
|
+
if (!opts.quiet) process.stdout.write(s);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const rows = scanReadSurfaces(opts.root);
|
|
199
|
+
const region = renderRegion(rows);
|
|
200
|
+
|
|
201
|
+
if (opts.write) {
|
|
202
|
+
const existing = _read(opts.root, DOC_REL) ?? `# Retrieval read surfaces\n`;
|
|
203
|
+
fs.mkdirSync(path.join(opts.root, path.dirname(DOC_REL)), { recursive: true });
|
|
204
|
+
fs.writeFileSync(path.join(opts.root, DOC_REL), spliceRegion(existing, region), 'utf-8');
|
|
205
|
+
process.stdout.write(`✍️ ${GATE}: wrote ${String(rows.length)} row(s) to ${DOC_REL}\n`);
|
|
206
|
+
return 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const findings: string[] = [];
|
|
210
|
+
|
|
211
|
+
// 1. Drift between the tree and the committed table.
|
|
212
|
+
const doc = _read(opts.root, DOC_REL);
|
|
213
|
+
if (doc === null) {
|
|
214
|
+
findings.push(`${DOC_REL} is missing — the generated table has no home.`);
|
|
215
|
+
} else {
|
|
216
|
+
const committed = extractRegion(doc);
|
|
217
|
+
if (committed === null) {
|
|
218
|
+
findings.push(`${DOC_REL} carries no \`${BEGIN_MARKER}\` … \`${END_MARKER}\` region.`);
|
|
219
|
+
} else if (committed !== region.trim()) {
|
|
220
|
+
findings.push(
|
|
221
|
+
`${DOC_REL} disagrees with the tree. The table is generated; regenerate it.`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 2. The floor's header points at the table rather than restating it.
|
|
227
|
+
const floor = _read(opts.root, FLOOR_REL);
|
|
228
|
+
if (floor === null) {
|
|
229
|
+
findings.push(`${FLOOR_REL} is missing — the floor this table describes is gone.`);
|
|
230
|
+
} else {
|
|
231
|
+
const header = leadingBlockComment(floor);
|
|
232
|
+
if (!header.includes(DOC_REL)) {
|
|
233
|
+
findings.push(
|
|
234
|
+
`${FLOOR_REL}'s header does not point at ${DOC_REL}. The coverage list is ` +
|
|
235
|
+
'generated; the header carries the pointer, never a second copy.',
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
const copies = headerSecondCopy(header, rows);
|
|
239
|
+
if (copies.length > 0) {
|
|
240
|
+
findings.push(
|
|
241
|
+
`${FLOOR_REL}'s header names ${String(copies.length)} module(s) the table also ` +
|
|
242
|
+
`names (${copies.join(', ')}). Two copies of this list is how it went stale ` +
|
|
243
|
+
'twice; the header points, the table lists.',
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 3. NO PERCENTAGE, and the gap register travels with the claim.
|
|
249
|
+
//
|
|
250
|
+
// This rule was REMOVED once, correctly: it watched a detector file that
|
|
251
|
+
// did not exist, and both review seats called it dead policy. It returns
|
|
252
|
+
// here with the detector, written against a file that exists, with its own
|
|
253
|
+
// rejecting self-test cases. If the detector is absent the rule is silent
|
|
254
|
+
// rather than red — a missing detector is Phase 3 being open, not a breach.
|
|
255
|
+
const detector = _read(opts.root, DETECTOR_REL);
|
|
256
|
+
if (detector !== null) {
|
|
257
|
+
if (publishesRate(detector)) {
|
|
258
|
+
findings.push(
|
|
259
|
+
`${DETECTOR_REL} publishes a recall or coverage figure. A rate needs a frozen ` +
|
|
260
|
+
'corpus of hiding techniques and none exists in this tree, so the number ' +
|
|
261
|
+
'would be an invented denominator a later reader quotes as measured coverage. ' +
|
|
262
|
+
'State the covered-channel list and the gap register instead.',
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
if (!detector.includes(GAP_REGISTER_SYMBOL)) {
|
|
266
|
+
findings.push(
|
|
267
|
+
`${DETECTOR_REL} does not define ${GAP_REGISTER_SYMBOL}. The register of what the ` +
|
|
268
|
+
"detector does NOT catch belongs in the detector's own file — a detector " +
|
|
269
|
+
'documented without its gaps is how the coverage claim becomes a false green.',
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
for (const [rel, src] of [
|
|
273
|
+
[FLOOR_REL, floor],
|
|
274
|
+
[DOC_REL, doc],
|
|
275
|
+
] as const) {
|
|
276
|
+
if (src === null) continue;
|
|
277
|
+
if (!src.includes('structural_hiding')) continue; // claims nothing
|
|
278
|
+
if (src.includes(GAP_REGISTER_SYMBOL)) continue;
|
|
279
|
+
findings.push(
|
|
280
|
+
`${rel} claims the structural coverage without citing ${GAP_REGISTER_SYMBOL}. ` +
|
|
281
|
+
'Every surface that claims this coverage cites the register alongside the ' +
|
|
282
|
+
'claim, so the caveat travels with the claim rather than staying in one file.',
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
reportScanned({
|
|
289
|
+
gate: GATE,
|
|
290
|
+
scanned: rows.length,
|
|
291
|
+
units: 'read surface(s)',
|
|
292
|
+
roots: [SCAN_ROOT],
|
|
293
|
+
});
|
|
294
|
+
} catch (e) {
|
|
295
|
+
if (e instanceof DeadScopeError) {
|
|
296
|
+
process.stdout.write(`❌ ${e.message}\n`);
|
|
297
|
+
return 1;
|
|
298
|
+
}
|
|
299
|
+
throw e;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (findings.length > 0) {
|
|
303
|
+
for (const f of findings) process.stdout.write(`❌ ${GATE}: ${f}\n`);
|
|
304
|
+
process.stdout.write(`\n${String(findings.length)} finding(s). Regenerate with:\n ${REGEN_COMMAND}\n`);
|
|
305
|
+
return 1;
|
|
306
|
+
}
|
|
307
|
+
const t = tally(rows);
|
|
308
|
+
say(
|
|
309
|
+
`✅ ${GATE}: ${String(rows.length)} read surface(s) in ${DOC_REL} match the tree ` +
|
|
310
|
+
`(${String(t.covered)} covered, ${String(t.uncovered)} uncovered, ` +
|
|
311
|
+
`${String(t.unclassified)} unclassified).\n`,
|
|
312
|
+
);
|
|
313
|
+
return 0;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* One rejecting case per way the three checks can be defeated.
|
|
318
|
+
*
|
|
319
|
+
* The empty-root case is the one that matters most here: a gate whose scan root
|
|
320
|
+
* moved reads zero surfaces and would otherwise certify a table it never
|
|
321
|
+
* compared. `assertScanned` has to turn that into a refusal, and only a fixture
|
|
322
|
+
* with no `src/scripts/` at all proves it does.
|
|
323
|
+
*/
|
|
324
|
+
function selfTest(): number {
|
|
325
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'read-surface-'));
|
|
326
|
+
const floor = _read(ROOT, FLOOR_REL) ?? '';
|
|
327
|
+
|
|
328
|
+
/** Plant a fixture root with a one-module scan tree plus the three inputs. */
|
|
329
|
+
const plant = (
|
|
330
|
+
name: string,
|
|
331
|
+
opts: {
|
|
332
|
+
module?: string;
|
|
333
|
+
mutateRegion?: (r: string) => string;
|
|
334
|
+
floorSrc?: string;
|
|
335
|
+
detectorSrc?: string;
|
|
336
|
+
noScanRoot?: boolean;
|
|
337
|
+
},
|
|
338
|
+
): string => {
|
|
339
|
+
const dir = path.join(tmp, name);
|
|
340
|
+
if (opts.noScanRoot !== true) {
|
|
341
|
+
fs.mkdirSync(path.join(dir, SCAN_ROOT, '_lib'), { recursive: true });
|
|
342
|
+
fs.writeFileSync(
|
|
343
|
+
path.join(dir, SCAN_ROOT, 'seed_surface.ts'),
|
|
344
|
+
opts.module ??
|
|
345
|
+
'export async function pull(u: string): Promise<string> {\n' +
|
|
346
|
+
' const r = await fetch(u);\n' +
|
|
347
|
+
' return await r.text();\n}\n',
|
|
348
|
+
'utf-8',
|
|
349
|
+
);
|
|
350
|
+
} else {
|
|
351
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
352
|
+
}
|
|
353
|
+
fs.mkdirSync(path.join(dir, path.dirname(FLOOR_REL)), { recursive: true });
|
|
354
|
+
fs.mkdirSync(path.join(dir, path.dirname(DOC_REL)), { recursive: true });
|
|
355
|
+
fs.writeFileSync(path.join(dir, FLOOR_REL), opts.floorSrc ?? floor, 'utf-8');
|
|
356
|
+
// The detector is planted ONLY where a case needs it. Absent, rule 3 is
|
|
357
|
+
// silent — which is itself the behaviour the last case below pins.
|
|
358
|
+
if (opts.detectorSrc !== undefined) {
|
|
359
|
+
fs.mkdirSync(path.join(dir, path.dirname(DETECTOR_REL)), { recursive: true });
|
|
360
|
+
fs.writeFileSync(path.join(dir, DETECTOR_REL), opts.detectorSrc, 'utf-8');
|
|
361
|
+
}
|
|
362
|
+
const region = renderRegion(scanReadSurfaces(dir));
|
|
363
|
+
fs.writeFileSync(
|
|
364
|
+
path.join(dir, DOC_REL),
|
|
365
|
+
`# fixture\n\n${BEGIN_MARKER}\n${(opts.mutateRegion ?? ((r) => r))(region)}\n${END_MARKER}\n`,
|
|
366
|
+
'utf-8',
|
|
367
|
+
);
|
|
368
|
+
return dir;
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const run = (root: string): number =>
|
|
372
|
+
runGateCli(ROOT, `src/scripts/${GATE}.ts`, ['--root', root], root);
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
return runSelfTest({
|
|
376
|
+
gate: GATE,
|
|
377
|
+
minCases: 10,
|
|
378
|
+
minRejectCases: 7,
|
|
379
|
+
cases: [
|
|
380
|
+
{
|
|
381
|
+
name: 'an unsanitized fetch-derived emit the committed table does not carry is rejected',
|
|
382
|
+
expect: 'reject',
|
|
383
|
+
run: () => {
|
|
384
|
+
const dir = plant('seeded', {});
|
|
385
|
+
fs.writeFileSync(
|
|
386
|
+
path.join(dir, SCAN_ROOT, 'seeded_leak.ts'),
|
|
387
|
+
'export async function grab(u: string): Promise<void> {\n' +
|
|
388
|
+
' const r = await fetch(u);\n' +
|
|
389
|
+
' writeFileSync("out.md", await r.text());\n}\n',
|
|
390
|
+
'utf-8',
|
|
391
|
+
);
|
|
392
|
+
return run(dir);
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
name: 'a hand-flipped coverage cell is rejected',
|
|
397
|
+
expect: 'reject',
|
|
398
|
+
run: () =>
|
|
399
|
+
run(
|
|
400
|
+
plant('flipped', {
|
|
401
|
+
mutateRegion: (r) => r.replace('**uncovered**', '**covered**'),
|
|
402
|
+
}),
|
|
403
|
+
),
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
name: "a header restating a module path the table lists is rejected",
|
|
407
|
+
expect: 'reject',
|
|
408
|
+
run: () =>
|
|
409
|
+
run(
|
|
410
|
+
plant('second-copy', {
|
|
411
|
+
floorSrc: `/**\n * Floor.\n * See ${DOC_REL}.\n * - seed_surface.ts — yes\n */\nexport const x = 1;\n`,
|
|
412
|
+
}),
|
|
413
|
+
),
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
name: 'a header with no pointer to the generated table is rejected',
|
|
417
|
+
expect: 'reject',
|
|
418
|
+
run: () => run(plant('no-pointer', { floorSrc: '/**\n * Floor.\n */\nexport const x = 1;\n' })),
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
name: 'an empty scan root is refused rather than certified',
|
|
422
|
+
expect: 'reject',
|
|
423
|
+
run: () => run(plant('empty-root', { noScanRoot: true })),
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
name: 'a detector publishing a coverage percentage is rejected',
|
|
427
|
+
expect: 'reject',
|
|
428
|
+
run: () =>
|
|
429
|
+
run(
|
|
430
|
+
plant('rate-published', {
|
|
431
|
+
floorSrc: `/**\n * Floor. Coverage list: ${DOC_REL}.\n * Gaps: ${GAP_REGISTER_SYMBOL}.\n */\nexport const x = 1;\n`,
|
|
432
|
+
detectorSrc:
|
|
433
|
+
`export const ${GAP_REGISTER_SYMBOL} = ['stylesheet-driven hiding'];\n` +
|
|
434
|
+
'/** Measured recall over the channel corpus: 94.2 %. */\n' +
|
|
435
|
+
'export const y = 1;\n',
|
|
436
|
+
}),
|
|
437
|
+
),
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
name: 'a detector with no gap register is rejected',
|
|
441
|
+
expect: 'reject',
|
|
442
|
+
run: () =>
|
|
443
|
+
run(
|
|
444
|
+
plant('no-register', {
|
|
445
|
+
floorSrc: `/**\n * Floor. Coverage list: ${DOC_REL}.\n * Gaps: ${GAP_REGISTER_SYMBOL}.\n */\nexport const x = 1;\n`,
|
|
446
|
+
detectorSrc: 'export const y = 1;\n',
|
|
447
|
+
}),
|
|
448
|
+
),
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
name: 'a floor claiming the coverage without citing the gap register is rejected',
|
|
452
|
+
expect: 'reject',
|
|
453
|
+
run: () =>
|
|
454
|
+
run(
|
|
455
|
+
plant('register-uncited', {
|
|
456
|
+
floorSrc: `/**\n * Floor. Coverage list: ${DOC_REL}. Structural: structural_hiding.\n */\nexport const x = 1;\n`,
|
|
457
|
+
detectorSrc: `export const ${GAP_REGISTER_SYMBOL} = ['stylesheet-driven hiding'];\n`,
|
|
458
|
+
}),
|
|
459
|
+
),
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
name: 'a CSS percentage inside a code span is NOT read as a published rate',
|
|
463
|
+
expect: 'accept',
|
|
464
|
+
run: () =>
|
|
465
|
+
run(
|
|
466
|
+
plant('rate-in-code-span', {
|
|
467
|
+
floorSrc: `/**\n * Floor. Coverage list: ${DOC_REL}.\n * Gaps: ${GAP_REGISTER_SYMBOL}.\n */\nexport const x = 1;\n`,
|
|
468
|
+
detectorSrc:
|
|
469
|
+
`export const ${GAP_REGISTER_SYMBOL} = [\n` +
|
|
470
|
+
" 'off-screen positioning — `transform: translate(-100%, 0)`',\n" +
|
|
471
|
+
'];\n',
|
|
472
|
+
}),
|
|
473
|
+
),
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
name: 'a detector that exists, publishes no rate and carries its register passes',
|
|
477
|
+
expect: 'accept',
|
|
478
|
+
run: () =>
|
|
479
|
+
run(
|
|
480
|
+
plant('detector-clean', {
|
|
481
|
+
floorSrc: `/**\n * Floor. Coverage list: ${DOC_REL}.\n * Gaps: ${GAP_REGISTER_SYMBOL}.\n */\nexport const x = 1;\n`,
|
|
482
|
+
detectorSrc: `export const ${GAP_REGISTER_SYMBOL} = ['stylesheet-driven hiding'];\n`,
|
|
483
|
+
}),
|
|
484
|
+
),
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
name: 'a regenerated table over an intact tree passes',
|
|
488
|
+
expect: 'accept',
|
|
489
|
+
run: () =>
|
|
490
|
+
run(
|
|
491
|
+
plant('good', {
|
|
492
|
+
floorSrc: `/**\n * Floor. Coverage list: ${DOC_REL}.\n */\nexport const x = 1;\n`,
|
|
493
|
+
}),
|
|
494
|
+
),
|
|
495
|
+
},
|
|
496
|
+
],
|
|
497
|
+
});
|
|
498
|
+
} finally {
|
|
499
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function _isCliEntry(): boolean {
|
|
504
|
+
if (process.argv[1] === undefined) return false;
|
|
505
|
+
if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) return true;
|
|
506
|
+
try {
|
|
507
|
+
return fs.realpathSync(_HERE) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
508
|
+
} catch {
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (_isCliEntry()) {
|
|
514
|
+
process.exit(main());
|
|
515
|
+
}
|
|
@@ -45,7 +45,8 @@ const _HERE = fileURLToPath(import.meta.url);
|
|
|
45
45
|
/**
|
|
46
46
|
* The folders a release question must read.
|
|
47
47
|
*
|
|
48
|
-
* Wider than `lint_roadmap_blockers`'
|
|
48
|
+
* Wider than `lint_roadmap_blockers`' glob on purpose — that gate reads the
|
|
49
|
+
* active tree plus `stubs/` and stops there (2026-09-28), while rule 28's
|
|
49
50
|
* per-folder lifecycle says a window does not disappear by moving the file, and
|
|
50
51
|
* a `later/` roadmap still refuses. `archive/` and `skipped/` are included
|
|
51
52
|
* because the move INTO them is refused while a window is open — a file that
|