@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,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-surface scanner — derive the sanitize floor's coverage list from the
|
|
3
|
+
* imports, not from intent
|
|
4
|
+
* (road-to-a-sanitize-list-that-is-generated, Phase 1).
|
|
5
|
+
*
|
|
6
|
+
* WHY A SCANNER AND NOT A LIST
|
|
7
|
+
* ----------------------------
|
|
8
|
+
* `retrieval_sanitize.ts` used to carry a hand-maintained prose list of the
|
|
9
|
+
* read surfaces it covers, with its own header stating the rule for anything
|
|
10
|
+
* not on it: uncovered. That list went wrong in both directions — a path that
|
|
11
|
+
* reaches the outside world and IS sanitized was absent from it, and two paths
|
|
12
|
+
* carrying fetched bytes toward a model-facing surface were neither listed nor
|
|
13
|
+
* covered. The header already recorded the same failure happening once before,
|
|
14
|
+
* for the same reason: a list maintained by hand records what its author meant,
|
|
15
|
+
* and the tree records what the code does.
|
|
16
|
+
*
|
|
17
|
+
* This module derives the list. A row cannot disagree with the tree, because
|
|
18
|
+
* the tree is the only thing it reads.
|
|
19
|
+
*
|
|
20
|
+
* WHAT A ROW MEANS, AND WHAT IT DOES NOT
|
|
21
|
+
* --------------------------------------
|
|
22
|
+
* A row says a module matched an INBOUND shape — a recognised way outside bytes
|
|
23
|
+
* enter this process — and whether that module imports the sanitize floor. It
|
|
24
|
+
* does not say the floor is applied to the right string, at the right point, or
|
|
25
|
+
* at all: an import is evidence of intent, and only a read of the module
|
|
26
|
+
* establishes the rest. `covered` here means "the floor is in scope in this
|
|
27
|
+
* module", never "this module is safe".
|
|
28
|
+
*
|
|
29
|
+
* THE MISSING-ROW FAILURE, AND WHY `unclassified` EXISTS
|
|
30
|
+
* -----------------------------------------------------
|
|
31
|
+
* A generated table carries an authority a hand-written one never claimed, so
|
|
32
|
+
* its worst failure is not a wrong row — it is an ABSENT one. A fetch shape the
|
|
33
|
+
* scanner does not recognise produces no row, and a missing row reads as "no
|
|
34
|
+
* surface here".
|
|
35
|
+
*
|
|
36
|
+
* Two things push back. Inbound shapes are plural and named (a `fetch`, a
|
|
37
|
+
* `node:http(s)` request, a subprocess of a remote-fetching tool, a parse of
|
|
38
|
+
* markup arriving on stdin) rather than a single pattern. And a WEAK shape —
|
|
39
|
+
* a network call reached through an injected or computed callee, which the
|
|
40
|
+
* strong detectors cannot see — still emits a row, marked `unclassified`, so an
|
|
41
|
+
* unrecognised shape appears in the table as a visible gap instead of as
|
|
42
|
+
* silence. The residual is stated in the table's own header rather than left
|
|
43
|
+
* for a reader to discover: a module whose network access is reached through a
|
|
44
|
+
* shape matching neither set is still absent.
|
|
45
|
+
*/
|
|
46
|
+
import * as fs from 'node:fs';
|
|
47
|
+
import * as path from 'node:path';
|
|
48
|
+
|
|
49
|
+
/** Coverage verdict for one read surface. */
|
|
50
|
+
export type SurfaceCoverage = 'covered' | 'uncovered' | 'unclassified';
|
|
51
|
+
|
|
52
|
+
/** One module that brings outside bytes in and hands a string onward. */
|
|
53
|
+
export interface ReadSurfaceRow {
|
|
54
|
+
/** Path relative to `src/scripts/`, POSIX separators. */
|
|
55
|
+
readonly module: string;
|
|
56
|
+
/** Inbound shape id that matched. */
|
|
57
|
+
readonly inbound: string;
|
|
58
|
+
/** Emit shape id that matched, or `undecided`. */
|
|
59
|
+
readonly emit: string;
|
|
60
|
+
readonly coverage: SurfaceCoverage;
|
|
61
|
+
/** Why the row reads the way it does — one clause, no paraphrase. */
|
|
62
|
+
readonly evidence: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface ShapeDetector {
|
|
66
|
+
readonly id: string;
|
|
67
|
+
/** True when this module's source carries the shape. */
|
|
68
|
+
readonly match: (src: string) => boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** True when the module imports node's HTTP client at all. */
|
|
72
|
+
function _importsNodeHttp(src: string): boolean {
|
|
73
|
+
return /from\s+['"]node:https?['"]/.test(src);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Strong inbound shapes — a recognised way outside bytes enter this process.
|
|
78
|
+
*
|
|
79
|
+
* Each is deliberately narrow. `await fetch(` rather than `fetch(` keeps a
|
|
80
|
+
* fetch call named inside a string literal (a spawned one-liner, a doc example)
|
|
81
|
+
* out of the table; the node-HTTP shape requires the import as well as the call
|
|
82
|
+
* so a `.get(` on an unrelated object cannot produce a row.
|
|
83
|
+
*/
|
|
84
|
+
export const STRONG_INBOUND: readonly ShapeDetector[] = [
|
|
85
|
+
{
|
|
86
|
+
id: 'network-fetch',
|
|
87
|
+
match: (s) => /\bawait\s+fetch\s*\(/.test(s),
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'node-http-request',
|
|
91
|
+
match: (s) => _importsNodeHttp(s) && /\bhttps?\s*\.\s*(?:get|request)\s*\(/.test(s),
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 'remote-subprocess',
|
|
95
|
+
match: (s) =>
|
|
96
|
+
/\b(?:spawnSync|spawn|execSync|execFileSync|execFile)\s*\(\s*\[?\s*['"`](?:curl|wget|gh)['"`]/.test(s),
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: 'stdin-markup-parse',
|
|
100
|
+
match: (s) =>
|
|
101
|
+
/\bprocess\s*\.\s*stdin\b/.test(s) && /&(?:amp|lt|gt|quot|nbsp|#x?[0-9a-f]+);/i.test(s),
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 'declared',
|
|
105
|
+
match: (s) => _DECLARATION_RE.test(s),
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* A module may declare its own inbound shape when no syntactic one captures it.
|
|
111
|
+
*
|
|
112
|
+
* The shapes above are all things the scanner can SEE. A host primitive cannot
|
|
113
|
+
* be seen: a subagent reply, an MCP tool result and a host file-read arrive
|
|
114
|
+
* through a channel this package does not own, so no call site in this tree
|
|
115
|
+
* spells them. Risk 1 of the roadmap that produced this scanner names exactly
|
|
116
|
+
* that class, and the answer it demands is a visible row rather than silence.
|
|
117
|
+
*
|
|
118
|
+
* The marker is a comment at the site — `// read-surface: <shape-id>` — not a
|
|
119
|
+
* line in a central list, which is the arrangement this whole module exists to
|
|
120
|
+
* retire. A declaration says only "outside bytes arrive here"; the coverage
|
|
121
|
+
* verdict is still read from the imports, so declaring a surface cannot make it
|
|
122
|
+
* read `covered`.
|
|
123
|
+
*/
|
|
124
|
+
const _DECLARATION_RE = /\/\/\s*read-surface:\s*([a-z0-9-]+)/;
|
|
125
|
+
|
|
126
|
+
/** `git` is deliberately NOT a remote command above. */
|
|
127
|
+
export const LOCAL_VCS_EXCLUSION =
|
|
128
|
+
'a `git` subprocess reads local repository state, not outside bytes, so it is not an ' +
|
|
129
|
+
'inbound shape. `git fetch` and `git ls-remote` move refs rather than content and are ' +
|
|
130
|
+
'excluded with the rest — a branch name is attacker-influenceable in principle, and if ' +
|
|
131
|
+
'that ever reaches a model-facing surface it needs its own row, not a widened matcher ' +
|
|
132
|
+
'that would re-add every local `git status` wrapper in the tree.';
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Weak inbound shapes — a network call the strong detectors cannot resolve.
|
|
136
|
+
*
|
|
137
|
+
* `await this._fetch(url, …)` is a real HTTP request behind an injected
|
|
138
|
+
* dependency, which is exactly the shape a strong matcher on `await fetch(`
|
|
139
|
+
* misses. It produces a row rather than silence; the verdict is
|
|
140
|
+
* `unclassified`, because the scanner cannot tell an injected fetch from an
|
|
141
|
+
* injected stub without executing it.
|
|
142
|
+
*/
|
|
143
|
+
export const WEAK_INBOUND: readonly ShapeDetector[] = [
|
|
144
|
+
{
|
|
145
|
+
id: 'indirect-fetch',
|
|
146
|
+
match: (s) => /\bawait\s+(?:this|self|[a-z_$][\w$]*)\s*\.\s*_?fetch\s*\(/i.test(s),
|
|
147
|
+
},
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
/** Recognised ways a module hands a string onward. */
|
|
151
|
+
export const EMIT_SHAPES: readonly ShapeDetector[] = [
|
|
152
|
+
{ id: 'file-write', match: (s) => /\b(?:writeFileSync|appendFileSync|writeFile)\s*\(/.test(s) },
|
|
153
|
+
{ id: 'stdout-write', match: (s) => /\bprocess\s*\.\s*stdout\s*\.\s*write\s*\(/.test(s) },
|
|
154
|
+
{ id: 'exported-api', match: (s) => /^export\s+(?:async\s+)?function\s/m.test(s) },
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
/** The sanitize floor, as an import. One hop; a transitive import is not counted. */
|
|
158
|
+
const _SANITIZE_IMPORT_RE =
|
|
159
|
+
/import\s*\{[^}]*\b(?:sanitize_text|sanitize_entry|sanitize_markup)\b[^}]*\}\s*from\s*['"][^'"]*retrieval_sanitize(?:\.js)?['"]/;
|
|
160
|
+
|
|
161
|
+
/** Test files under `src/scripts/` — fixtures, not surfaces that ship bytes. */
|
|
162
|
+
const _EXCLUDE_RE = /(?:^|\/)(?:__tests__|node_modules)\/|\.test\.ts$/;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Modules that define or read the coverage rather than being a surface.
|
|
166
|
+
*
|
|
167
|
+
* The scanner and its gate quote every detector pattern above as a literal, so
|
|
168
|
+
* they match their own matchers. Excluding them by name is the honest fix: a
|
|
169
|
+
* row for the scanner would be an artefact of the scanner reading itself, and
|
|
170
|
+
* carrying it would train a reader to skip rows.
|
|
171
|
+
*/
|
|
172
|
+
const _SELF_MODULES: ReadonlySet<string> = new Set([
|
|
173
|
+
'_lib/retrieval_sanitize.ts',
|
|
174
|
+
'_lib/read_surface_scan.ts',
|
|
175
|
+
'check_read_surface_coverage.ts',
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Raised when a directory under the scan root cannot be read.
|
|
180
|
+
*
|
|
181
|
+
* The walk used to swallow the error and return, which made an INCOMPLETE scan
|
|
182
|
+
* indistinguishable from a complete one: a permission error on one subtree
|
|
183
|
+
* silently removed every module under it from the table, and the gate then
|
|
184
|
+
* reported the shortened list as matching the tree. An independent review named
|
|
185
|
+
* it; the direction is the same one `assertScanned` enforces at the corpus
|
|
186
|
+
* level, applied to the traversal that builds the corpus.
|
|
187
|
+
*/
|
|
188
|
+
export class ScanTraversalError extends Error {
|
|
189
|
+
constructor(readonly dir: string, cause: unknown) {
|
|
190
|
+
super(`read_surface_scan: cannot read ${dir} — the scan is incomplete, not clean (${String(cause)})`);
|
|
191
|
+
this.name = 'ScanTraversalError';
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Directory reader, injectable for one reason: to test the failure.
|
|
197
|
+
*
|
|
198
|
+
* The fail-closed branch below can only be exercised by a `readdirSync` that
|
|
199
|
+
* throws, and on ESM `vi.spyOn(fs, 'readdirSync')` cannot redefine the
|
|
200
|
+
* namespace property. The alternative — chmod-000 on a real directory — is not
|
|
201
|
+
* portable, because under root the mode is no barrier and the test then fails
|
|
202
|
+
* in exactly the environment it was written to survive. A narrow seam is the
|
|
203
|
+
* honest shape: production passes nothing and gets `fs.readdirSync`.
|
|
204
|
+
*/
|
|
205
|
+
export type ReadDir = (dir: string) => fs.Dirent[];
|
|
206
|
+
|
|
207
|
+
const _defaultReadDir: ReadDir = (dir) => fs.readdirSync(dir, { withFileTypes: true });
|
|
208
|
+
|
|
209
|
+
function _walk(dir: string, base: string, out: string[], readDir: ReadDir): void {
|
|
210
|
+
let entries: fs.Dirent[];
|
|
211
|
+
try {
|
|
212
|
+
entries = readDir(dir);
|
|
213
|
+
} catch (exc) {
|
|
214
|
+
throw new ScanTraversalError(dir, exc);
|
|
215
|
+
}
|
|
216
|
+
for (const e of entries) {
|
|
217
|
+
const abs = path.join(dir, e.name);
|
|
218
|
+
const rel = path.relative(base, abs).split(path.sep).join('/');
|
|
219
|
+
if (e.isDirectory()) {
|
|
220
|
+
_walk(abs, base, out, readDir);
|
|
221
|
+
} else if (e.isFile() && rel.endsWith('.ts') && !rel.endsWith('.d.ts') && !_EXCLUDE_RE.test(rel)) {
|
|
222
|
+
out.push(rel);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** The scan root, repo-relative. Published so the gate can name it in an error. */
|
|
228
|
+
export const SCAN_ROOT = 'src/scripts';
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Scan every TypeScript module under `src/scripts/` and return one row per
|
|
232
|
+
* module that both brings outside bytes in and hands a string onward.
|
|
233
|
+
*
|
|
234
|
+
* Rows are sorted by module path so the rendered table is stable across
|
|
235
|
+
* filesystems.
|
|
236
|
+
*/
|
|
237
|
+
export function scanReadSurfaces(repoRoot: string, readDir: ReadDir = _defaultReadDir): ReadSurfaceRow[] {
|
|
238
|
+
const base = path.join(repoRoot, SCAN_ROOT);
|
|
239
|
+
const files: string[] = [];
|
|
240
|
+
_walk(base, base, files, readDir);
|
|
241
|
+
files.sort();
|
|
242
|
+
|
|
243
|
+
const rows: ReadSurfaceRow[] = [];
|
|
244
|
+
for (const rel of files) {
|
|
245
|
+
if (_SELF_MODULES.has(rel)) continue;
|
|
246
|
+
let src: string;
|
|
247
|
+
try {
|
|
248
|
+
src = fs.readFileSync(path.join(base, rel), 'utf-8');
|
|
249
|
+
} catch {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const strong = STRONG_INBOUND.find((d) => d.match(src));
|
|
253
|
+
const weak = strong === undefined ? WEAK_INBOUND.find((d) => d.match(src)) : undefined;
|
|
254
|
+
if (strong === undefined && weak === undefined) continue;
|
|
255
|
+
// A declared surface reports the shape its own marker names, so the
|
|
256
|
+
// table says `ingested-corpus` rather than the detector's own id.
|
|
257
|
+
const strongId =
|
|
258
|
+
strong?.id === 'declared' ? (_DECLARATION_RE.exec(src)?.[1] ?? 'declared') : strong?.id;
|
|
259
|
+
|
|
260
|
+
const emit = EMIT_SHAPES.find((d) => d.match(src));
|
|
261
|
+
const sanitized = _SANITIZE_IMPORT_RE.test(src);
|
|
262
|
+
|
|
263
|
+
if (weak !== undefined) {
|
|
264
|
+
rows.push({
|
|
265
|
+
module: rel,
|
|
266
|
+
inbound: weak.id,
|
|
267
|
+
emit: emit?.id ?? 'undecided',
|
|
268
|
+
coverage: 'unclassified',
|
|
269
|
+
evidence: 'network access through a callee the scanner cannot resolve — read the module',
|
|
270
|
+
});
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (emit === undefined) {
|
|
274
|
+
rows.push({
|
|
275
|
+
module: rel,
|
|
276
|
+
inbound: strongId ?? 'unknown',
|
|
277
|
+
emit: 'undecided',
|
|
278
|
+
coverage: 'unclassified',
|
|
279
|
+
evidence: 'inbound bytes with no recognised emit — the onward path is undecided',
|
|
280
|
+
});
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
rows.push({
|
|
284
|
+
module: rel,
|
|
285
|
+
inbound: strongId ?? 'unknown',
|
|
286
|
+
emit: emit.id,
|
|
287
|
+
coverage: sanitized ? 'covered' : 'uncovered',
|
|
288
|
+
evidence: sanitized
|
|
289
|
+
? 'imports the sanitize floor'
|
|
290
|
+
: 'no sanitize import — fetched bytes reach the emit unfiltered',
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
return rows;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Counts by verdict, for the gate's scanned line and for the table header. */
|
|
297
|
+
export function tally(rows: readonly ReadSurfaceRow[]): Record<SurfaceCoverage, number> {
|
|
298
|
+
const out: Record<SurfaceCoverage, number> = { covered: 0, uncovered: 0, unclassified: 0 };
|
|
299
|
+
for (const r of rows) out[r.coverage] += 1;
|
|
300
|
+
return out;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Render the rows as the markdown table body committed to the contract doc. */
|
|
304
|
+
export function renderTable(rows: readonly ReadSurfaceRow[]): string {
|
|
305
|
+
const lines = [
|
|
306
|
+
'| module (under `src/scripts/`) | inbound shape | emit shape | coverage | evidence |',
|
|
307
|
+
'|---|---|---|---|---|',
|
|
308
|
+
];
|
|
309
|
+
for (const r of rows) {
|
|
310
|
+
lines.push(
|
|
311
|
+
`| \`${r.module}\` | \`${r.inbound}\` | \`${r.emit}\` | **${r.coverage}** | ${r.evidence} |`,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
const t = tally(rows);
|
|
315
|
+
lines.push('');
|
|
316
|
+
lines.push(
|
|
317
|
+
`${String(rows.length)} read surface(s): ` +
|
|
318
|
+
`${String(t.covered)} covered, ${String(t.uncovered)} uncovered, ` +
|
|
319
|
+
`${String(t.unclassified)} unclassified.`,
|
|
320
|
+
);
|
|
321
|
+
return lines.join('\n');
|
|
322
|
+
}
|
|
@@ -574,7 +574,8 @@ export function section_publication_blockers(
|
|
|
574
574
|
'fails, so a missing line means the collection failed during `task release`. ' +
|
|
575
575
|
'Fix the collection, then render the number and put it on the section — ' +
|
|
576
576
|
`the release commit already exists on ${where}, so amend it there. Do not ` +
|
|
577
|
-
'invent the count: `npx vitest list |
|
|
577
|
+
'invent the count: `npx vitest list --staticParse=false | grep -c .` is the same ' +
|
|
578
|
+
'probe the writer runs.',
|
|
578
579
|
);
|
|
579
580
|
}
|
|
580
581
|
return out;
|
|
@@ -16,26 +16,58 @@
|
|
|
16
16
|
* Codepoint classes are shared with `lint_hidden_unicode` (one source of truth
|
|
17
17
|
* for what counts as a hidden-instruction vector).
|
|
18
18
|
*
|
|
19
|
-
* WHERE IT ACTUALLY RUNS —
|
|
20
|
-
* honest: an earlier version of this header named surfaces by intent, and the
|
|
21
|
-
* legacy-envelope gap below went unnoticed for exactly that reason. Every
|
|
22
|
-
* claim here is backed by a probe row in
|
|
23
|
-
* `agents/evidence/reports/sanitize-floor-wiring.md`:
|
|
19
|
+
* WHERE IT ACTUALLY RUNS — read the table, which is GENERATED:
|
|
24
20
|
*
|
|
25
|
-
*
|
|
26
|
-
* - `memory_get_v1()` — yes (same)
|
|
27
|
-
* - MCP `memory_lookup` / `memory_get` tools — inherited from the two above
|
|
28
|
-
* - `retrieve()` / `retrieve_with_meta()` — yes, since the S0.0 wiring fix;
|
|
29
|
-
* before it they emitted every vector intact, and so did the CLI default
|
|
30
|
-
* (`--envelope legacy`), which is the path the rules document
|
|
31
|
-
* - inter-agent channels (`ai_team` / `ai_council` model replies) — the
|
|
32
|
-
* inbound parse choke point only; the subagent boundary itself is a HOST
|
|
33
|
-
* primitive this package cannot reach (see the report, § S0.0b)
|
|
21
|
+
* docs/contracts/retrieval-read-surfaces.md
|
|
34
22
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
23
|
+
* This header used to carry that list inline, and the list was wrong in BOTH
|
|
24
|
+
* directions: a path that reaches the outside world and IS sanitized was
|
|
25
|
+
* absent from it, and two paths carrying fetched bytes toward a model-facing
|
|
26
|
+
* surface were neither listed nor covered. An earlier version had already
|
|
27
|
+
* failed the same way — it named surfaces by intent, and the legacy-envelope
|
|
28
|
+
* gap went unnoticed for exactly that reason. Twice is a property of the
|
|
29
|
+
* mechanism, not of the entries, which is why the list is now derived from the
|
|
30
|
+
* import graph by `check_read_surface_coverage` and this header points at it
|
|
31
|
+
* instead of copying it. A divergence between the two FAILS that gate; two
|
|
32
|
+
* copies is how it went stale.
|
|
33
|
+
*
|
|
34
|
+
* Anything absent from the table is uncovered, and a module the generator
|
|
35
|
+
* cannot classify appears there as an `unclassified` ROW rather than being
|
|
36
|
+
* omitted — a missing row would read as no surface at all.
|
|
37
|
+
*
|
|
38
|
+
* One boundary the table cannot move: the subagent boundary itself is a HOST
|
|
39
|
+
* primitive this package cannot reach, so the inter-agent channels are covered
|
|
40
|
+
* at the inbound parse choke point and nowhere else.
|
|
41
|
+
*
|
|
42
|
+
* loss_class: ephemeral-lossy
|
|
43
|
+
*
|
|
44
|
+
* `sanitize_text` drops hidden-instruction codepoints, control-char noise and
|
|
45
|
+
* invisible fillers, and caps the result at MAX_FIELD_CHARS. None of that is
|
|
46
|
+
* reconstructible from the output, and for the dropped vectors the
|
|
47
|
+
* unrecoverability IS the guarantee — a retrieval path that could hand back
|
|
48
|
+
* the vector it stripped would not be a floor. So the class is
|
|
49
|
+
* `ephemeral-lossy` for the module, even though `sanitize_markup` below
|
|
50
|
+
* returns structured removals for the layer it composes: a class names what a
|
|
51
|
+
* caller can recover from the output, and `sanitize_text` returns a string.
|
|
52
|
+
*
|
|
53
|
+
* THE STRUCTURAL LAYER IS A SEPARATE MODULE. Everything below is a CODEPOINT
|
|
54
|
+
* floor. Fetched markup also hides content STRUCTURALLY — an HTML comment, a
|
|
55
|
+
* `<template>` block, a `style="display:none"` span — which no codepoint
|
|
56
|
+
* predicate can see. That class is covered by `_lib/structural_hiding.ts`,
|
|
57
|
+
* composed with this floor by `sanitize_markup` below, and it is covered for
|
|
58
|
+
* fetched MARKUP only: a caller holding plain text needs nothing from it.
|
|
59
|
+
*
|
|
60
|
+
* WHAT THAT LAYER STILL DOES NOT SEE is a register in its own file —
|
|
61
|
+
* `STRUCTURAL_HIDING_GAPS` — and every surface claiming the coverage cites the
|
|
62
|
+
* register alongside the claim, because a detector cited without its gaps is
|
|
63
|
+
* how the claim becomes a false green. No recall or coverage percentage is
|
|
64
|
+
* published for it anywhere; a rate needs a frozen corpus and none exists.
|
|
65
|
+
* `aria-hidden` is deliberately NOT one of its channels
|
|
66
|
+
* (`STRUCTURAL_HIDING_NON_CHANNELS` says why).
|
|
37
67
|
*/
|
|
38
68
|
import { _classify } from '../lint_hidden_unicode.js';
|
|
69
|
+
import { strip_structural_hiding } from './structural_hiding.js';
|
|
70
|
+
import type { StructuralRemoval } from './structural_hiding.js';
|
|
39
71
|
import { TOKEN_RE, classifyToken } from './confusables.js';
|
|
40
72
|
|
|
41
73
|
/** Hard per-field length cap — bounds a runaway/adversarial body. */
|
|
@@ -85,6 +117,35 @@ export function sanitize_text(s: string): string {
|
|
|
85
117
|
return out.length > MAX_FIELD_CHARS ? out.slice(0, MAX_FIELD_CHARS) : out;
|
|
86
118
|
}
|
|
87
119
|
|
|
120
|
+
/**
|
|
121
|
+
* The fetched-markup floor: structural pre-pass, then the codepoint floor.
|
|
122
|
+
*
|
|
123
|
+
* ORDER IS LOAD-BEARING. The structural pass runs FIRST, because a codepoint
|
|
124
|
+
* floor applied first would sanitize text that is about to be discarded and,
|
|
125
|
+
* worse, would leave a hidden subtree looking clean. Running it second means
|
|
126
|
+
* every byte that survives the structural pass still clears the codepoint
|
|
127
|
+
* floor — including the text that was only reachable once markup was removed.
|
|
128
|
+
*
|
|
129
|
+
* Use this for fetched MARKUP. For plain text `sanitize_text` is the whole
|
|
130
|
+
* floor and this adds nothing but a parse.
|
|
131
|
+
*
|
|
132
|
+
* The removals are RETURNED, not logged and not swallowed: a caller debugging
|
|
133
|
+
* a corrupted result needs to know what went and where it was. See the removal
|
|
134
|
+
* policy in `structural_hiding.ts`.
|
|
135
|
+
*/
|
|
136
|
+
export function sanitize_markup(markup: string): {
|
|
137
|
+
text: string;
|
|
138
|
+
removals: readonly StructuralRemoval[];
|
|
139
|
+
truncated: boolean;
|
|
140
|
+
} {
|
|
141
|
+
const stripped = strip_structural_hiding(markup);
|
|
142
|
+
return {
|
|
143
|
+
text: sanitize_text(stripped.text),
|
|
144
|
+
removals: stripped.removals,
|
|
145
|
+
truncated: stripped.truncated,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
88
149
|
/**
|
|
89
150
|
* Report the INVISIBLE layer without removing it.
|
|
90
151
|
*
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shell shapes that mean "this path is being WRITTEN", shared by the
|
|
3
|
+
* PreToolUse guards that refuse a write to a governed file.
|
|
4
|
+
*
|
|
5
|
+
* WHY IT IS SHARED. `block_kernel_rule_writes.ts` grew this parser after the
|
|
6
|
+
* S0.2 spike measured a real gap: a guard keyed on the TOOL NAME let
|
|
7
|
+
* `Bash sed -i … src/rules/commit-policy.md` reach the outcome the Write
|
|
8
|
+
* branch refuses, and a two-step `Write` → `Bash mv` sequence did the same with
|
|
9
|
+
* every individual step allowed. `block_plumbing_writes.ts`
|
|
10
|
+
* (road-to-a-kernel-that-guards-its-plumbing 1.2) refuses a different file set
|
|
11
|
+
* against the same shapes. Two copies of a shell-shape parser is two places a
|
|
12
|
+
* verb can be added to one and forgotten in the other, and the guard that
|
|
13
|
+
* silently stopped recognising `tee` would look exactly like the guard that
|
|
14
|
+
* never had to.
|
|
15
|
+
*
|
|
16
|
+
* DELIBERATELY NARROW (council 2026-08-02 cut, option ii, recorded on the
|
|
17
|
+
* kernel guard and inherited here unchanged): only redirection into the path,
|
|
18
|
+
* an in-place `sed`, a `tee`/`truncate`/`rm`/`shred` naming it, or a
|
|
19
|
+
* `mv`/`cp`/`install`/`rsync` whose DESTINATION it is. Reads stay allowed —
|
|
20
|
+
* `cat`, `grep`, `head`, `diff`, `git show` carry none of these shapes — and no
|
|
21
|
+
* attempt is made to understand arbitrary shell. Recognising every conceivable
|
|
22
|
+
* write verb would make this a shell sandbox, which is the failure mode the
|
|
23
|
+
* council named.
|
|
24
|
+
*
|
|
25
|
+
* KNOWN-OPEN, stated rather than implied: a write routed through an
|
|
26
|
+
* interpreter (`python -c`, a heredoc fed to `sh`) carries none of these
|
|
27
|
+
* shapes and is not detected. This is a guard, not write mediation.
|
|
28
|
+
*/
|
|
29
|
+
import * as path from 'node:path';
|
|
30
|
+
|
|
31
|
+
/** In-place mutators that name their target as a plain argument. */
|
|
32
|
+
const MUTATOR_VERBS: ReadonlySet<string> = new Set(['sed', 'tee', 'truncate', 'rm', 'shred']);
|
|
33
|
+
|
|
34
|
+
/** Verbs whose LAST positional argument is the destination. */
|
|
35
|
+
const DEST_LAST_VERBS: ReadonlySet<string> = new Set(['mv', 'cp', 'install', 'rsync']);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The first governed target a command WRITES, or `null`.
|
|
39
|
+
*
|
|
40
|
+
* @param command the raw shell string off the tool envelope
|
|
41
|
+
* @param match classifies one token: a non-null return names the governed
|
|
42
|
+
* target that token resolves to, and is what this returns
|
|
43
|
+
*/
|
|
44
|
+
export function shellWriteTarget<T>(command: string, match: (token: string) => T | null): T | null {
|
|
45
|
+
const tokens = command.split(/\s+/).filter((t) => t.length > 0);
|
|
46
|
+
const stripped = tokens.map((t) => t.replace(/^['"]|['"]$/g, ''));
|
|
47
|
+
|
|
48
|
+
// Cheap reject first: no governed path in the string at all.
|
|
49
|
+
if (!stripped.some((t) => match(t) !== null)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 1. Redirection into the path: `> p`, `>> p`, `>p`, `>>p`.
|
|
54
|
+
for (let i = 0; i < stripped.length; i += 1) {
|
|
55
|
+
const tok = stripped[i] as string;
|
|
56
|
+
const inline = /^>{1,2}(.+)$/.exec(tok);
|
|
57
|
+
if (inline) {
|
|
58
|
+
const r = match(inline[1] as string);
|
|
59
|
+
if (r !== null) {
|
|
60
|
+
return r;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (tok === '>' || tok === '>>') {
|
|
64
|
+
const next = stripped[i + 1];
|
|
65
|
+
if (typeof next === 'string') {
|
|
66
|
+
const r = match(next);
|
|
67
|
+
if (r !== null) {
|
|
68
|
+
return r;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 2. A mutator verb anywhere in the pipeline, with the path as an argument.
|
|
75
|
+
// `sed` only counts in its in-place form — `sed 's/x/y/' file` prints.
|
|
76
|
+
const verbs = new Set<string>();
|
|
77
|
+
let prevWasSeparator = true;
|
|
78
|
+
for (const tok of stripped) {
|
|
79
|
+
if (['&&', '||', ';', '|'].includes(tok)) {
|
|
80
|
+
prevWasSeparator = true;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (prevWasSeparator && !tok.startsWith('-')) {
|
|
84
|
+
verbs.add(path.basename(tok));
|
|
85
|
+
prevWasSeparator = false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const sedInPlace = verbs.has('sed') && stripped.some((t) => /^-i/.test(t));
|
|
89
|
+
for (const verb of verbs) {
|
|
90
|
+
if (verb === 'sed' && !sedInPlace) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (MUTATOR_VERBS.has(verb)) {
|
|
94
|
+
const hit = stripped.map(match).find((r) => r !== null);
|
|
95
|
+
if (hit !== undefined && hit !== null) {
|
|
96
|
+
return hit;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (DEST_LAST_VERBS.has(verb)) {
|
|
100
|
+
const last = stripped[stripped.length - 1] as string;
|
|
101
|
+
const r = match(last);
|
|
102
|
+
if (r !== null) {
|
|
103
|
+
return r;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|