@event4u/agent-config 14.13.0 → 14.14.1
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 +138 -376
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/inbox.md +36 -0
- package/dist/agent-src/commands/pr/create.md +16 -1
- package/dist/agent-src/contexts/execution/terminal-states.md +25 -4
- package/dist/agent-src/rules/code-comment-discipline.md +6 -0
- package/dist/agent-src/rules/missing-skill-recovery.md +2 -2
- package/dist/agent-src/rules/token-budget-discipline.md +2 -2
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +6 -6
- package/dist/agent-src/scripts/guarded_baseline.ts +22 -7
- package/dist/agent-src/scripts/stubs_due.ts +118 -2
- package/dist/agent-src/scripts/update_roadmap_progress.ts +8 -8
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +1 -0
- package/dist/cli-delegate/{chunk-ILMUPLGW.js → chunk-7RXIKF62.js} +159 -86
- package/dist/cli-delegate/cmd_conformance.js +39 -2
- package/dist/cli-delegate/cmd_doctor.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +7 -7
- 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 +1696 -812
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +297 -115
- package/docs/guidelines/agent-infra/guarded-baseline.md +1 -1
- package/docs/guidelines/code-clarity.md +18 -0
- package/package.json +1 -1
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +6 -6
- package/src/agent-src/scripts/guarded_baseline.ts +22 -7
- package/src/agent-src/scripts/stubs_due.ts +118 -2
- package/src/agent-src/scripts/update_roadmap_progress.ts +8 -8
- package/src/config/assurance-capability-registry.json +1 -1
- package/src/config/estate-count-budget.json +20 -1
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +211 -1
- package/src/config/gate-reachability-exemptions.json +1 -0
- package/src/config/gate-violation-baselines.json +9 -3
- package/src/config/preamble-payload-budget.json +19 -11
- package/src/config/rule-stub-ceilings.json +14 -7
- package/src/config/web-launch-readiness.json +4 -2
- package/src/scripts/_cli/_doctor_emit.ts +71 -0
- package/src/scripts/_cli/_doctor_strict.ts +163 -0
- package/src/scripts/_cli/cmd_doctor.ts +16 -37
- package/src/scripts/_dispatch.bash +11 -4
- package/src/scripts/_lib/activation_ladder.ts +47 -3
- package/src/scripts/_lib/activation_receipt_producer.ts +341 -0
- package/src/scripts/_lib/adr_frontmatter.ts +26 -0
- package/src/scripts/_lib/branch_convergence.ts +186 -0
- package/src/scripts/_lib/candidate_pair_delta.ts +349 -0
- package/src/scripts/_lib/carrier_status.ts +118 -0
- package/src/scripts/_lib/cascade_stage_enumeration.ts +92 -0
- package/src/scripts/_lib/context_observation.ts +132 -0
- package/src/scripts/_lib/continuation_ladder.ts +188 -0
- package/src/scripts/_lib/corpus_manifest.ts +726 -0
- package/src/scripts/_lib/delivery_arm_experiment.ts +18 -2
- package/src/scripts/_lib/estate_carrier_cases.ts +72 -0
- package/src/scripts/_lib/evaluation_cascade.ts +452 -0
- package/src/scripts/_lib/evolution_roi.ts +562 -0
- package/src/scripts/_lib/experiment_binding.ts +95 -0
- package/src/scripts/_lib/harness_evolution_guards.ts +10 -0
- package/src/scripts/_lib/ignored_blocker.ts +8 -1
- package/src/scripts/_lib/lexical_shortlist.ts +215 -0
- package/src/scripts/_lib/llm_candidate_proposer.ts +461 -0
- package/src/scripts/_lib/llm_proposer_transport.ts +153 -0
- package/src/scripts/_lib/outcome_envelope.ts +6 -1
- package/src/scripts/_lib/outcome_vocabularies.ts +135 -5
- package/src/scripts/_lib/pathology_archive.ts +393 -0
- package/src/scripts/_lib/promotion_capability.ts +315 -0
- package/src/scripts/_lib/promotion_evidence.ts +414 -0
- package/src/scripts/_lib/promotion_review.ts +294 -0
- package/src/scripts/_lib/prompt_shape.ts +92 -0
- package/src/scripts/_lib/release_highlights.ts +79 -1
- package/src/scripts/_lib/release_material.ts +125 -2
- package/src/scripts/_lib/repeated_failure.ts +19 -2
- package/src/scripts/_lib/routing_corpus.ts +307 -0
- package/src/scripts/_lib/routing_index_input.ts +104 -0
- package/src/scripts/_lib/rule_injection.ts +22 -1
- package/src/scripts/_lib/runtime_journal.ts +11 -4
- package/src/scripts/_lib/semantic_noop.ts +167 -0
- package/src/scripts/activation_receipt.ts +258 -0
- package/src/scripts/ai_council/cross_exam.ts +128 -0
- package/src/scripts/ai_council/explain_route.ts +157 -0
- package/src/scripts/ai_council/information_gain.ts +293 -0
- package/src/scripts/ai_council/leakage_corpus.ts +385 -0
- package/src/scripts/ai_council/leakage_patterns.ts +453 -0
- package/src/scripts/ai_council/minority_retention.ts +171 -0
- package/src/scripts/ai_council/recouncil_savings.ts +299 -0
- package/src/scripts/ai_council/replay_route.ts +223 -0
- package/src/scripts/ai_council/reviewer_assignment.ts +279 -0
- package/src/scripts/ai_council/routing_training_row.ts +197 -0
- package/src/scripts/ai_council/synthesis_strategy.ts +159 -0
- package/src/scripts/check_adr_frontmatter.ts +4 -9
- package/src/scripts/check_agent_artifact_location.ts +337 -0
- package/src/scripts/check_artefact_count_messaging.ts +74 -8
- package/src/scripts/check_enforcement_coverage.ts +40 -1
- package/src/scripts/check_estate_count.ts +205 -24
- package/src/scripts/check_release_highlights.ts +113 -14
- package/src/scripts/check_roadmap_trackable.ts +17 -2
- package/src/scripts/check_web_launch_readiness.ts +310 -20
- package/src/scripts/corpus_manifest.ts +235 -0
- package/src/scripts/evolution_lab.ts +234 -9
- package/src/scripts/generate_capability_matrix.ts +16 -9
- package/src/scripts/grade_target_readiness.ts +221 -2
- package/src/scripts/hook_effect_doctor.ts +47 -4
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +28 -6
- package/src/scripts/hooks/comment_discipline_hook.ts +146 -0
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/run_continuation_hook.ts +78 -88
- package/src/scripts/hooks/session_eol_hook.ts +12 -1
- package/src/scripts/hooks/turn_end_gate_hook.ts +9 -1
- package/src/scripts/install-hooks.sh +18 -0
- package/src/scripts/language_mirror_hook.ts +14 -2
- package/src/scripts/lint_carrier_integrity.ts +820 -0
- package/src/scripts/lint_code_comments.ts +748 -0
- package/src/scripts/lint_plan_risk_register.ts +39 -19
- package/src/scripts/lint_promotion_paths.ts +779 -0
- package/src/scripts/llm_propose.ts +256 -0
- package/src/scripts/measure_delivery_sets.ts +361 -0
- package/src/scripts/measure_routing_signal.ts +307 -0
- package/src/scripts/model_rule_injection.ts +19 -3
- package/src/scripts/probe_council_retention.ts +495 -0
- package/src/scripts/release.ts +21 -37
- package/src/scripts/release_drill.ts +199 -0
- package/src/scripts/release_env.ts +35 -0
- package/src/scripts/release_publication.ts +142 -0
- package/src/scripts/roadmap_context.ts +15 -0
- package/src/scripts/skill_scout.ts +633 -0
- package/src/scripts/sync_pr_branch.ts +521 -42
|
@@ -21,14 +21,16 @@
|
|
|
21
21
|
* (`claim:web-launch-readiness-finds-more`, status `unbacked`). Absent the
|
|
22
22
|
* opt-in, this exits 0 and says why rather than auditing.
|
|
23
23
|
*
|
|
24
|
-
* Exit codes: 0 = no critical/high finding
|
|
25
|
-
*
|
|
24
|
+
* Exit codes: 0 = no critical/high finding and nothing left undecided (or not
|
|
25
|
+
* enabled) · 1 = a blocking finding, OR an applicable check the instrument could
|
|
26
|
+
* not decide · 2 = usage or a dead scan scope.
|
|
26
27
|
*/
|
|
27
28
|
import * as fs from 'node:fs';
|
|
28
29
|
import * as path from 'node:path';
|
|
29
30
|
import process from 'node:process';
|
|
30
31
|
import { fileURLToPath } from 'node:url';
|
|
31
32
|
|
|
33
|
+
import { asOf } from './_lib/as_of.js';
|
|
32
34
|
import { DeadScopeError, reportScanned } from './_lib/scan_scope.js';
|
|
33
35
|
|
|
34
36
|
const HERE = fileURLToPath(import.meta.url);
|
|
@@ -42,7 +44,12 @@ export type Tier = 'critical' | 'high' | 'medium' | 'situational';
|
|
|
42
44
|
*
|
|
43
45
|
* `applies_to` decides WHETHER a check applies; a region decides HOW SEVERELY.
|
|
44
46
|
* A legal page is not more or less *applicable* in Germany — it is more or less
|
|
45
|
-
* *optional*, because
|
|
47
|
+
* *optional*, because German and EU law make it owed rather than advisable. The
|
|
48
|
+
* two citations behind that sentence are NOT restated here: they live in the
|
|
49
|
+
* escalation row's `authority` field in `web-launch-readiness.json`, where a
|
|
50
|
+
* `review_by` date can lapse them. That is the whole point of the
|
|
51
|
+
* `ddg-citation-authority` decision — a statute in prose cited law already dead
|
|
52
|
+
* for twenty-seven months and nothing could see it.
|
|
46
53
|
* Modelling it as a tier escalation keeps one check with one implementation and
|
|
47
54
|
* puts the jurisdiction where it belongs: on the consequence.
|
|
48
55
|
*/
|
|
@@ -56,6 +63,10 @@ export interface CheckDef {
|
|
|
56
63
|
why: string;
|
|
57
64
|
remediation: string;
|
|
58
65
|
verification: string;
|
|
66
|
+
/** Statute + where it can be read. Required once the row asserts a legal basis. */
|
|
67
|
+
authority?: string;
|
|
68
|
+
/** ISO date the citation must be re-read by. A lapsed date FAILS the loader. */
|
|
69
|
+
review_by?: string;
|
|
59
70
|
}
|
|
60
71
|
|
|
61
72
|
export interface Finding {
|
|
@@ -75,6 +86,22 @@ export interface Skipped {
|
|
|
75
86
|
reason: string;
|
|
76
87
|
}
|
|
77
88
|
|
|
89
|
+
/**
|
|
90
|
+
* An applicable check the instrument could not decide.
|
|
91
|
+
*
|
|
92
|
+
* NOT a finding (nothing was established) and NOT a pass (nothing was cleared).
|
|
93
|
+
* The third state exists because a detector that guesses on an undecidable input
|
|
94
|
+
* is worse than one that abstains — and because a pass this checker did not earn
|
|
95
|
+
* is exactly the silent-green defect its own header names.
|
|
96
|
+
*/
|
|
97
|
+
export interface Undecided {
|
|
98
|
+
check: string;
|
|
99
|
+
tier: Tier;
|
|
100
|
+
/** `file:line` the instrument stopped at, so the reader can look at it. */
|
|
101
|
+
location: string;
|
|
102
|
+
reason: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
78
105
|
/** A tier the region axis moved, and why. Reported so the axis is visible. */
|
|
79
106
|
export interface Escalated {
|
|
80
107
|
check: string;
|
|
@@ -93,6 +120,8 @@ export interface Report {
|
|
|
93
120
|
passed: string[];
|
|
94
121
|
/** Applicable checks with no implementation yet — never counted as passed. */
|
|
95
122
|
unimplemented: string[];
|
|
123
|
+
/** Applicable, implemented, and undecidable on THIS input. Never a pass. */
|
|
124
|
+
unknown: Undecided[];
|
|
96
125
|
/** Tiers the region axis raised. Empty on `unspecified`, by construction. */
|
|
97
126
|
escalated: Escalated[];
|
|
98
127
|
scanned_files: number;
|
|
@@ -103,6 +132,141 @@ export interface Escalation {
|
|
|
103
132
|
region: Region;
|
|
104
133
|
to: Tier;
|
|
105
134
|
why: string;
|
|
135
|
+
/** Same contract as `CheckDef` — an escalation row is a legal claim too. */
|
|
136
|
+
authority?: string;
|
|
137
|
+
review_by?: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* -- 2.1/2.2: a legal claim carries its own authority and expiry ------------ */
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* A statute reference: a named instrument, or a numbered article/section.
|
|
144
|
+
*
|
|
145
|
+
* The list is deliberately short and abbreviation-shaped. A broader "any legal
|
|
146
|
+
* word" match would fire on `regulatory exposure` and turn every jurisdictional
|
|
147
|
+
* sentence into a citation the gate then demands a source for — which is how a
|
|
148
|
+
* scoped gate becomes a blanket one nobody can satisfy. The numbered-provision
|
|
149
|
+
* arm (`Art. 13`, `§ 5`) catches an instrument the list does not name yet.
|
|
150
|
+
*
|
|
151
|
+
* `TMG` is retained as a token on purpose, and this is a HISTORICAL NOTE rather
|
|
152
|
+
* than a citation: the statute was superseded by the DDG in May 2024, so a row
|
|
153
|
+
* that names it again is exactly what this detector must still catch.
|
|
154
|
+
*/
|
|
155
|
+
export const STATUTE_RE =
|
|
156
|
+
/\b(TMG|DDG|TTDSG|TDDDG|DSGVO|GDPR|BDSG|RDG|UWG|BGB|HGB|StGB|CCPA|CPRA|HIPAA|ePrivacy|DSA|DMA|BFSG|EAA)\b|(?:\bArt\.|\bArticle\b|§)\s*\d/;
|
|
157
|
+
|
|
158
|
+
/** A config row that may assert a legal basis: a check, or a region escalation. */
|
|
159
|
+
export interface LegalRow {
|
|
160
|
+
kind: 'check' | 'escalation';
|
|
161
|
+
id: string;
|
|
162
|
+
why: string;
|
|
163
|
+
authority?: string;
|
|
164
|
+
review_by?: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Today, as an ISO date, through the repository's one sanctioned clock seam.
|
|
171
|
+
*
|
|
172
|
+
* NOT `new Date()`: a lapsed-date verdict read off the wall clock is a verdict
|
|
173
|
+
* that greens on Monday and reds on Friday from the same tree, which is the
|
|
174
|
+
* defect `lint_deterministic_time` exists to keep out of a gate. `asOf()`
|
|
175
|
+
* resolves `--as-of`, `AC_AS_OF`, the commit date under CI, and only then the
|
|
176
|
+
* wall clock — with its own non-reproducibility warning attached.
|
|
177
|
+
*/
|
|
178
|
+
export function asOfToday(): string {
|
|
179
|
+
return asOf().toISOString().slice(0, 10);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Every row of the config that could carry a legal claim, in report order. */
|
|
183
|
+
export function legalRowsOf(doc: {
|
|
184
|
+
checks?: CheckDef[];
|
|
185
|
+
regions?: { escalations?: Escalation[] };
|
|
186
|
+
}): LegalRow[] {
|
|
187
|
+
const rows: LegalRow[] = [];
|
|
188
|
+
for (const c of doc.checks ?? []) {
|
|
189
|
+
rows.push({
|
|
190
|
+
kind: 'check',
|
|
191
|
+
id: c.id,
|
|
192
|
+
why: c.why ?? '',
|
|
193
|
+
...(c.authority === undefined ? {} : { authority: c.authority }),
|
|
194
|
+
...(c.review_by === undefined ? {} : { review_by: c.review_by }),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
for (const e of doc.regions?.escalations ?? []) {
|
|
198
|
+
rows.push({
|
|
199
|
+
kind: 'escalation',
|
|
200
|
+
id: `${e.check}@${e.region}`,
|
|
201
|
+
why: e.why ?? '',
|
|
202
|
+
...(e.authority === undefined ? {} : { authority: e.authority }),
|
|
203
|
+
...(e.review_by === undefined ? {} : { review_by: e.review_by }),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return rows;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The legal-row contract, as a list of violations (empty = clean).
|
|
211
|
+
*
|
|
212
|
+
* Three obligations, and the third is the one that makes the other two more
|
|
213
|
+
* than decoration:
|
|
214
|
+
*
|
|
215
|
+
* 1. a row whose `why` NAMES a statute must carry `authority` and `review_by`;
|
|
216
|
+
* 2. `authority` and `review_by` travel together — a citation with no expiry
|
|
217
|
+
* is the comment this contract exists to replace, and an expiry with no
|
|
218
|
+
* citation dates nothing. This half is what keeps the contract intact when
|
|
219
|
+
* the statute moves OUT of the prose and INTO `authority`;
|
|
220
|
+
* 3. a `review_by` on or after `asOf` is live; anything earlier is LAPSED and
|
|
221
|
+
* is a violation rather than a warning. A date nothing reads is a comment,
|
|
222
|
+
* and this repository already documents a warning ignored eighteen times.
|
|
223
|
+
*
|
|
224
|
+
* A row that asserts no legal basis is not policed. The gate is scoped, not
|
|
225
|
+
* blanket: demanding a citation from `A framework default error page loses the
|
|
226
|
+
* visitor` would make the contract unsatisfiable and therefore ignorable.
|
|
227
|
+
*/
|
|
228
|
+
export function legalRowViolations(rows: readonly LegalRow[], asOf: string): string[] {
|
|
229
|
+
const out: string[] = [];
|
|
230
|
+
for (const r of rows) {
|
|
231
|
+
const namesStatute = STATUTE_RE.test(r.why);
|
|
232
|
+
const hasAuthority = (r.authority ?? '').trim().length > 0;
|
|
233
|
+
const hasDate = (r.review_by ?? '').trim().length > 0;
|
|
234
|
+
const where = `${r.kind} "${r.id}"`;
|
|
235
|
+
// One violation per MISSING FIELD, never one per rule that noticed it.
|
|
236
|
+
// Dropping `authority` from a statute row trips both obligation 1 and
|
|
237
|
+
// obligation 2, and reporting it twice would make the count a property
|
|
238
|
+
// of the rule set rather than of the config.
|
|
239
|
+
const isLegal = namesStatute || hasAuthority || hasDate;
|
|
240
|
+
if (isLegal && !hasAuthority) {
|
|
241
|
+
out.push(
|
|
242
|
+
`${where}: carries no \`authority\` — ` +
|
|
243
|
+
(namesStatute
|
|
244
|
+
? 'its `why` names a statute, and a citation with no source is a comment rather than a claim anyone can re-check.'
|
|
245
|
+
: 'a `review_by` with no citation dates nothing.'),
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
if (isLegal && !hasDate) {
|
|
249
|
+
out.push(
|
|
250
|
+
`${where}: carries no \`review_by\` — ` +
|
|
251
|
+
(namesStatute
|
|
252
|
+
? 'its `why` names a statute, and the citation has no date by which it must be re-read.'
|
|
253
|
+
: 'an undated citation cannot go stale visibly.'),
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
if (!hasDate) continue;
|
|
257
|
+
const date = (r.review_by ?? '').trim();
|
|
258
|
+
if (!ISO_DATE_RE.test(date) || Number.isNaN(Date.parse(date))) {
|
|
259
|
+
out.push(`${where}: \`review_by\` is "${date}", which is not an ISO YYYY-MM-DD date.`);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (date < asOf) {
|
|
263
|
+
out.push(
|
|
264
|
+
`${where}: \`review_by\` lapsed on ${date} (as of ${asOf}) — re-read the citation, ` +
|
|
265
|
+
'then move the date. Overdue is a finding, not a silent pass.',
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return out;
|
|
106
270
|
}
|
|
107
271
|
|
|
108
272
|
export function loadConfig(root = REPO_ROOT): {
|
|
@@ -144,6 +308,15 @@ export function loadConfig(root = REPO_ROOT): {
|
|
|
144
308
|
'situational tier for a legally owed page.',
|
|
145
309
|
);
|
|
146
310
|
}
|
|
311
|
+
const legal = legalRowViolations(legalRowsOf(doc), asOfToday());
|
|
312
|
+
if (legal.length > 0) {
|
|
313
|
+
// A gate that audited a site while shipping an unbacked legal claim of
|
|
314
|
+
// its own would be auditing from a position it has not earned.
|
|
315
|
+
throw new DeadScopeError(
|
|
316
|
+
'check_web_launch_readiness',
|
|
317
|
+
`${CONFIG_REL} ships a legal claim the row cannot back:\n - ${legal.join('\n - ')}`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
147
320
|
return { checks, siteTypes, regions, escalations };
|
|
148
321
|
}
|
|
149
322
|
|
|
@@ -479,17 +652,44 @@ const requiredLegalPages: Impl = (files) => {
|
|
|
479
652
|
};
|
|
480
653
|
|
|
481
654
|
/**
|
|
482
|
-
* Analytics loads only behind consent.
|
|
655
|
+
* Analytics loads only behind consent — INCLUDING the load order (3.1).
|
|
656
|
+
*
|
|
657
|
+
* The row's own `verification` field asks for the runtime assertion: *"Load the
|
|
658
|
+
* page with consent declined and assert no request to the analytics origin."*
|
|
659
|
+
* A static reader cannot load a page. The strongest assertion it CAN make is
|
|
660
|
+
* source order within one document, so that is what it makes — and where even
|
|
661
|
+
* that is undecidable it returns `unknown` (3.2) rather than a pass.
|
|
483
662
|
*
|
|
484
|
-
*
|
|
485
|
-
*
|
|
486
|
-
*
|
|
487
|
-
*
|
|
663
|
+
* What the ordering pass covers, stated so the message can say it: the position
|
|
664
|
+
* of the measurement tag relative to the gate script INSIDE one file. What it
|
|
665
|
+
* does not cover: runtime order, deferred or dynamic imports, order established
|
|
666
|
+
* by a bundler across files, and anything a minifier put on one line — each of
|
|
667
|
+
* which produces an `unknown`, never a silent pass.
|
|
488
668
|
*/
|
|
489
669
|
const ANALYTICS_RE = /(googletagmanager\.com|google-analytics\.com|gtag\(|plausible\.io|matomo|segment\.com|hotjar)/i;
|
|
490
670
|
const CONSENT_RE = /(consent|cookiebanner|cookie-banner|cookieconsent|klaro|usercentrics|borlabs|osano)/i;
|
|
491
|
-
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* A line that can actually LOAD something, as opposed to prose about loading.
|
|
674
|
+
*
|
|
675
|
+
* `consent-order-bad` says the word *consent* in its `<meta description>` and
|
|
676
|
+
* still fires: a sentence describing a gate is not a gate, and a positional
|
|
677
|
+
* detector that counted it would report a correctly ordered page while the tag
|
|
678
|
+
* fires first. Applied to markup only — every line of a `.js` file is code.
|
|
679
|
+
*/
|
|
680
|
+
const EXECUTABLE_LINE_RE = /<script\b|\bsrc\s*=|\bimport\b|\brequire\s*\(|\.js\b/i;
|
|
681
|
+
|
|
682
|
+
/** Past this, a line is a bundler artefact and its internal order is not source order. */
|
|
683
|
+
const MINIFIED_LINE_CHARS = 400;
|
|
684
|
+
|
|
685
|
+
interface ConsentOrder {
|
|
686
|
+
hits: Located[];
|
|
687
|
+
undecided: Located[];
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function analyseConsentOrder(files: readonly SourceFile[]): ConsentOrder {
|
|
492
691
|
const hits: Located[] = [];
|
|
692
|
+
const undecided: Located[] = [];
|
|
493
693
|
// Prose is excluded from BOTH sides, and the fixture is why: a `.md` file
|
|
494
694
|
// in the build tree describing the consent banner matched CONSENT_RE, so
|
|
495
695
|
// the check reported a pass on a fixture seeded to fire it. Documentation
|
|
@@ -497,21 +697,92 @@ const analyticsAndConsent: Impl = (files) => {
|
|
|
497
697
|
// runs the other way, so an analytics name mentioned in a README must not
|
|
498
698
|
// count as analytics either.
|
|
499
699
|
const code = files.filter((f) => !/\.md$/i.test(f.base));
|
|
500
|
-
const
|
|
501
|
-
|
|
700
|
+
const loadable = (f: SourceFile, line: string): boolean => !isPage(f) || EXECUTABLE_LINE_RE.test(line);
|
|
701
|
+
const firstLoad = (f: SourceFile, re: RegExp): number =>
|
|
702
|
+
f.lines.findIndex((l) => re.test(l) && loadable(f, l));
|
|
703
|
+
const anyConsent = code.some((f) => firstLoad(f, CONSENT_RE) >= 0);
|
|
704
|
+
|
|
502
705
|
for (const f of code) {
|
|
503
|
-
f
|
|
504
|
-
|
|
706
|
+
const a = firstLoad(f, ANALYTICS_RE);
|
|
707
|
+
if (a < 0) continue;
|
|
708
|
+
if (!anyConsent) {
|
|
709
|
+
// Unchanged from the pre-3.1 behaviour, and still the sharpest
|
|
710
|
+
// finding available: nothing in the build can gate the tag.
|
|
711
|
+
f.lines.forEach((line, i) => {
|
|
712
|
+
if (!ANALYTICS_RE.test(line) || !loadable(f, line)) return;
|
|
713
|
+
hits.push({
|
|
714
|
+
file: f.rel,
|
|
715
|
+
line: i + 1,
|
|
716
|
+
text:
|
|
717
|
+
`${line.trim().slice(0, 120)} — analytics is loaded here and no consent mechanism ` +
|
|
718
|
+
'was found anywhere in the build, so nothing can gate it. Source order was not the ' +
|
|
719
|
+
'question: there is no gate to be ordered against.',
|
|
720
|
+
});
|
|
721
|
+
});
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
const c = firstLoad(f, CONSENT_RE);
|
|
725
|
+
const aLine = f.lines[a] ?? '';
|
|
726
|
+
if (c < 0) {
|
|
727
|
+
undecided.push({
|
|
728
|
+
file: f.rel,
|
|
729
|
+
line: a + 1,
|
|
730
|
+
text:
|
|
731
|
+
'analytics is loaded here and the consent mechanism lives in another file. Which of ' +
|
|
732
|
+
'the two the browser runs first is decided by the bundler and the page, not by source ' +
|
|
733
|
+
'order across files, and a static read cannot establish it.',
|
|
734
|
+
});
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
if (c === a) {
|
|
738
|
+
undecided.push({
|
|
739
|
+
file: f.rel,
|
|
740
|
+
line: a + 1,
|
|
741
|
+
text:
|
|
742
|
+
'the analytics tag and the consent gate are on the SAME line, so line order cannot ' +
|
|
743
|
+
'separate them. Ordering is undecidable on this input.',
|
|
744
|
+
});
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
if (aLine.length > MINIFIED_LINE_CHARS) {
|
|
748
|
+
undecided.push({
|
|
749
|
+
file: f.rel,
|
|
750
|
+
line: a + 1,
|
|
751
|
+
text:
|
|
752
|
+
`this line is ${String(aLine.length)} characters, i.e. minified — the order inside a ` +
|
|
753
|
+
'bundler artefact is not the source order this pass reads.',
|
|
754
|
+
});
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
if (a < c) {
|
|
505
758
|
hits.push({
|
|
506
759
|
file: f.rel,
|
|
507
|
-
line:
|
|
760
|
+
line: a + 1,
|
|
508
761
|
text:
|
|
509
|
-
`${
|
|
510
|
-
|
|
762
|
+
`${aLine.trim().slice(0, 120)} — the analytics tag is at line ${String(a + 1)}, ` +
|
|
763
|
+
`ABOVE the consent gate at line ${String(c + 1)} of the same file, so it is reached ` +
|
|
764
|
+
'before consent can be recorded. Source order inside this file is what was checked; ' +
|
|
765
|
+
'runtime order, dynamic imports and cross-file bundler order were not.',
|
|
511
766
|
});
|
|
512
|
-
}
|
|
767
|
+
}
|
|
513
768
|
}
|
|
514
|
-
return hits;
|
|
769
|
+
return { hits, undecided };
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const analyticsAndConsent: Impl = (files) => analyseConsentOrder(files).hits;
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Checks that can report `unknown`, and the probe that decides when they do.
|
|
776
|
+
*
|
|
777
|
+
* A separate table rather than a widened `Impl` signature: the invariant that an
|
|
778
|
+
* EMPTY `Located[]` means "applied and found nothing" is what keeps the
|
|
779
|
+
* silent-green defect out of `IMPLS`, and folding a third state into that return
|
|
780
|
+
* type would put the invariant back in play for all eight implementations.
|
|
781
|
+
*/
|
|
782
|
+
type Undecide = (files: readonly SourceFile[]) => Located[];
|
|
783
|
+
|
|
784
|
+
export const UNDECIDABLE: Readonly<Record<string, Undecide>> = {
|
|
785
|
+
'analytics-and-consent-wiring': (files) => analyseConsentOrder(files).undecided,
|
|
515
786
|
};
|
|
516
787
|
|
|
517
788
|
/** First line containing `needle`, 1-indexed; 1 when absent (an absence has no line). */
|
|
@@ -560,6 +831,7 @@ export function audit(
|
|
|
560
831
|
const skipped: Skipped[] = [];
|
|
561
832
|
const passed: string[] = [];
|
|
562
833
|
const unimplemented: string[] = [];
|
|
834
|
+
const unknown: Undecided[] = [];
|
|
563
835
|
const escalated: Escalated[] = [];
|
|
564
836
|
// ONE walk for every check. Eight walkers over the same bytes is eight
|
|
565
837
|
// times the IO and eight chances to disagree about what counts as a file.
|
|
@@ -586,7 +858,11 @@ export function audit(
|
|
|
586
858
|
impl === undefined
|
|
587
859
|
? scanIndexability(buildDir).hits
|
|
588
860
|
: impl(files);
|
|
589
|
-
|
|
861
|
+
const undecided = UNDECIDABLE[c.id]?.(files) ?? [];
|
|
862
|
+
for (const u of undecided) {
|
|
863
|
+
unknown.push({ check: c.id, tier, location: `${u.file}:${String(u.line)}`, reason: u.text });
|
|
864
|
+
}
|
|
865
|
+
if (hits.length === 0 && undecided.length === 0) {
|
|
590
866
|
passed.push(c.id);
|
|
591
867
|
continue;
|
|
592
868
|
}
|
|
@@ -609,6 +885,7 @@ export function audit(
|
|
|
609
885
|
skipped,
|
|
610
886
|
passed,
|
|
611
887
|
unimplemented,
|
|
888
|
+
unknown,
|
|
612
889
|
escalated,
|
|
613
890
|
scanned_files: files.length,
|
|
614
891
|
};
|
|
@@ -637,6 +914,15 @@ export function render(r: Report): string {
|
|
|
637
914
|
out.push(` verification: ${x.verification}`);
|
|
638
915
|
}
|
|
639
916
|
}
|
|
917
|
+
if (r.unknown.length > 0) {
|
|
918
|
+
// Printed with the findings rather than with the passes, because an
|
|
919
|
+
// undecided check is an open question about the site — not a clearance.
|
|
920
|
+
out.push('', `UNDECIDED — applicable, audited, and not decidable from this build (${String(r.unknown.length)})`);
|
|
921
|
+
for (const u of r.unknown) {
|
|
922
|
+
out.push(` ${u.location} ${u.check}`);
|
|
923
|
+
out.push(` unknown: ${u.reason}`);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
640
926
|
if (r.passed.length > 0) out.push('', `PASSED: ${r.passed.join(', ')}`);
|
|
641
927
|
if (r.unimplemented.length > 0) {
|
|
642
928
|
out.push('', `NOT YET IMPLEMENTED (applicable, not audited): ${r.unimplemented.join(', ')}`);
|
|
@@ -697,7 +983,11 @@ export function main(argv: string[] = process.argv.slice(2), root = REPO_ROOT):
|
|
|
697
983
|
process.stdout.write(`${render(r)}\n`);
|
|
698
984
|
}
|
|
699
985
|
const blocking = r.findings.filter((f) => f.tier === 'critical' || f.tier === 'high');
|
|
700
|
-
|
|
986
|
+
// An UNDECIDED applicable check exits non-zero whatever its tier. Exiting 0
|
|
987
|
+
// would report the run as clean on a question the instrument never answered,
|
|
988
|
+
// which is the same silent-green failure as counting an unimplemented check
|
|
989
|
+
// as passed — the tier axis grades findings, and this is not one.
|
|
990
|
+
return blocking.length > 0 || r.unknown.length > 0 ? 1 : 0;
|
|
701
991
|
}
|
|
702
992
|
|
|
703
993
|
if (path.resolve(process.argv[1] ?? '') === path.resolve(HERE)) {
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* `corpus_manifest` — pin the metered-proposer experiment's subject, and check
|
|
4
|
+
* that a fresh checkout reproduces it.
|
|
5
|
+
*
|
|
6
|
+
* Two verbs and no third. `capture` writes the pin from the tree as it stands;
|
|
7
|
+
* `verify` re-captures and reports every difference against a pin on disk.
|
|
8
|
+
* There is deliberately no `update` verb: a pin that can be refreshed in place
|
|
9
|
+
* is a pin that silently follows the tree, which is the property it exists not
|
|
10
|
+
* to have.
|
|
11
|
+
*
|
|
12
|
+
* `verify` exits 3 when the SUBJECT differs and 0 when only the explanatory
|
|
13
|
+
* fields do. That split is the whole point of the exit code — a different
|
|
14
|
+
* runtime version is a fact worth printing and is not a reason to refuse a
|
|
15
|
+
* comparison, while a different subject means the two runs measured different
|
|
16
|
+
* things and no amount of green elsewhere repairs it.
|
|
17
|
+
*
|
|
18
|
+
* Nothing here spends: it reads files and shells out to `git` for the commit.
|
|
19
|
+
*/
|
|
20
|
+
import * as fs from 'node:fs';
|
|
21
|
+
import * as os from 'node:os';
|
|
22
|
+
import * as path from 'node:path';
|
|
23
|
+
import process from 'node:process';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
|
|
26
|
+
import { partitionActive } from '../install/partitionEligibility.js';
|
|
27
|
+
import {
|
|
28
|
+
type CorpusManifest,
|
|
29
|
+
captureManifest,
|
|
30
|
+
diffManifests,
|
|
31
|
+
parseManifest,
|
|
32
|
+
serialiseManifest,
|
|
33
|
+
subjectsEquivalent,
|
|
34
|
+
} from './_lib/corpus_manifest.js';
|
|
35
|
+
|
|
36
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
37
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
38
|
+
|
|
39
|
+
export const EXIT_OK = 0;
|
|
40
|
+
export const EXIT_ERROR = 1;
|
|
41
|
+
export const EXIT_USAGE = 2;
|
|
42
|
+
/** The subject differs. Distinct from a plain error so a runner can branch. */
|
|
43
|
+
export const EXIT_SUBJECT_DIFFERS = 3;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The enumeration rule this manifest pins under, as an ID rather than prose.
|
|
47
|
+
*
|
|
48
|
+
* It is part of the digest, so changing the rule changes the subject even when
|
|
49
|
+
* every file hash is unchanged. That is correct: a corpus of the same five
|
|
50
|
+
* files selected by a different rule is a different experiment, and a digest
|
|
51
|
+
* that could not see the difference would let the selection semantics be
|
|
52
|
+
* amended without the pin noticing.
|
|
53
|
+
*/
|
|
54
|
+
export const ENUMERATION_RULE_ID = 'claude-rules-md-bytesorted-first-N/v1';
|
|
55
|
+
|
|
56
|
+
const USAGE = `usage: corpus_manifest capture --out FILE [--limit N]
|
|
57
|
+
corpus_manifest verify --manifest FILE
|
|
58
|
+
|
|
59
|
+
capture write the subject pin from the tree as it stands
|
|
60
|
+
verify re-capture and report every difference against FILE
|
|
61
|
+
|
|
62
|
+
--limit N corpus size, on CAPTURE only; defaults to max_candidates from the
|
|
63
|
+
pre-registered budget, which is the ceiling the run guard aborts
|
|
64
|
+
on. verify re-captures at the size its own manifest pinned, so it
|
|
65
|
+
REFUSES --limit rather than accepting a flag it cannot honour
|
|
66
|
+
|
|
67
|
+
Exit: 0 ok · 1 error · 2 usage · 3 the SUBJECT differs
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The corpus size, read from the pre-registered budget rather than defaulted.
|
|
72
|
+
*
|
|
73
|
+
* Hardcoding five here would put a second copy of a pre-registered number in a
|
|
74
|
+
* file nobody would think to check when the budget changed, and the two would
|
|
75
|
+
* disagree in the direction that matters: a manifest pinning more subjects than
|
|
76
|
+
* the run guard admits describes an experiment that cannot be run.
|
|
77
|
+
*/
|
|
78
|
+
export function defaultLimit(repoRoot: string = REPO_ROOT): number {
|
|
79
|
+
const raw = JSON.parse(
|
|
80
|
+
fs.readFileSync(path.join(repoRoot, 'src', 'config', 'harness-evolution-budget.json'), 'utf-8'),
|
|
81
|
+
) as { budget?: { max_candidates?: unknown } };
|
|
82
|
+
const n = raw.budget?.max_candidates;
|
|
83
|
+
if (typeof n !== 'number' || !Number.isInteger(n) || n <= 0) {
|
|
84
|
+
throw new Error('max_candidates missing or not a positive integer in the pre-registered budget');
|
|
85
|
+
}
|
|
86
|
+
return n;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface Parsed {
|
|
90
|
+
verb?: string;
|
|
91
|
+
out?: string;
|
|
92
|
+
manifest?: string;
|
|
93
|
+
limit?: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function parseArgs(argv: readonly string[]): Parsed | string {
|
|
97
|
+
const p: Parsed = {};
|
|
98
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
99
|
+
const a = argv[i] as string;
|
|
100
|
+
if (!a.startsWith('--') && p.verb === undefined) {
|
|
101
|
+
p.verb = a;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const v = argv[i + 1];
|
|
105
|
+
if (v === undefined || v.startsWith('--')) return `${a} requires a value`;
|
|
106
|
+
i += 1;
|
|
107
|
+
if (a === '--out') p.out = v;
|
|
108
|
+
else if (a === '--manifest') p.manifest = v;
|
|
109
|
+
else if (a === '--limit') {
|
|
110
|
+
const n = Number(v);
|
|
111
|
+
if (!Number.isInteger(n) || n <= 0) return '--limit must be a positive integer';
|
|
112
|
+
p.limit = n;
|
|
113
|
+
} else return `unknown option '${a}'`;
|
|
114
|
+
}
|
|
115
|
+
if (p.verb === 'verify' && p.limit !== undefined) {
|
|
116
|
+
return (
|
|
117
|
+
'verify does not take --limit: it re-captures at the size its own manifest pinned, ' +
|
|
118
|
+
'because a re-capture at another corpus size is a comparison against another subject. ' +
|
|
119
|
+
'Accepting the flag and then using the pinned size reported green on a run that had ' +
|
|
120
|
+
'not honoured it'
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return p;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* One real capture over the real tree.
|
|
128
|
+
*
|
|
129
|
+
* This is the one caller for which the process globals `partitionActive` reads
|
|
130
|
+
* — the host layer under the running user's home, and the install lockfile —
|
|
131
|
+
* ARE this run's own host, so it is the one caller that passes the predicate.
|
|
132
|
+
* Everywhere else the field stays `null` rather than recording some other
|
|
133
|
+
* machine's state under this tree's name.
|
|
134
|
+
*/
|
|
135
|
+
function capture(limit: number): CorpusManifest {
|
|
136
|
+
return captureManifest({
|
|
137
|
+
repoRoot: REPO_ROOT,
|
|
138
|
+
userHome: process.env['HOME'] ?? os.homedir(),
|
|
139
|
+
limit,
|
|
140
|
+
enumerationRule: ENUMERATION_RULE_ID,
|
|
141
|
+
partitionActive,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function main(argv?: string[]): number {
|
|
146
|
+
const args = argv ?? process.argv.slice(2);
|
|
147
|
+
if (args.length === 0 || args.includes('-h') || args.includes('--help')) {
|
|
148
|
+
process.stdout.write(USAGE);
|
|
149
|
+
return args.length === 0 ? EXIT_USAGE : EXIT_OK;
|
|
150
|
+
}
|
|
151
|
+
const parsed = parseArgs(args);
|
|
152
|
+
if (typeof parsed === 'string') {
|
|
153
|
+
process.stderr.write(`corpus_manifest: error: ${parsed}\n${USAGE}`);
|
|
154
|
+
return EXIT_USAGE;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (parsed.verb === 'capture') {
|
|
158
|
+
if (parsed.out === undefined) {
|
|
159
|
+
process.stderr.write(`corpus_manifest: error: capture requires --out FILE\n${USAGE}`);
|
|
160
|
+
return EXIT_USAGE;
|
|
161
|
+
}
|
|
162
|
+
let m: CorpusManifest;
|
|
163
|
+
try {
|
|
164
|
+
m = capture(parsed.limit ?? defaultLimit());
|
|
165
|
+
} catch (e) {
|
|
166
|
+
process.stderr.write(`corpus_manifest: capture refused — ${(e as Error).message}\n`);
|
|
167
|
+
return EXIT_ERROR;
|
|
168
|
+
}
|
|
169
|
+
fs.mkdirSync(path.dirname(path.resolve(parsed.out)), { recursive: true });
|
|
170
|
+
fs.writeFileSync(parsed.out, serialiseManifest(m), 'utf-8');
|
|
171
|
+
process.stdout.write(
|
|
172
|
+
`corpus_manifest: wrote ${parsed.out} · subject_digest=${m.subject_digest.slice(0, 16)} · ` +
|
|
173
|
+
`${String(m.included.length)} subject(s) · ${String(m.produced.length)} produced · ` +
|
|
174
|
+
`${String(m.user_scope.filter((t) => t.byte_identical).length)} byte-identical ` +
|
|
175
|
+
`twin(s), ${String(m.user_scope.filter((t) => t.causes_skip).length)} of them ` +
|
|
176
|
+
`causing a skip under ${String(m.generator_config['projection.scope_dedup'])}\n`,
|
|
177
|
+
);
|
|
178
|
+
if (m.tree_dirty) {
|
|
179
|
+
process.stdout.write(
|
|
180
|
+
'corpus_manifest: WARNING the working tree is dirty, so the recorded commit does not ' +
|
|
181
|
+
'describe the bytes that were hashed\n',
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
return EXIT_OK;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (parsed.verb === 'verify') {
|
|
188
|
+
if (parsed.manifest === undefined) {
|
|
189
|
+
process.stderr.write(`corpus_manifest: error: verify requires --manifest FILE\n${USAGE}`);
|
|
190
|
+
return EXIT_USAGE;
|
|
191
|
+
}
|
|
192
|
+
let expected: CorpusManifest;
|
|
193
|
+
let actual: CorpusManifest;
|
|
194
|
+
try {
|
|
195
|
+
expected = parseManifest(JSON.parse(fs.readFileSync(parsed.manifest, 'utf-8')));
|
|
196
|
+
actual = capture(expected.included.length);
|
|
197
|
+
} catch (e) {
|
|
198
|
+
process.stderr.write(`corpus_manifest: verify refused — ${(e as Error).message}\n`);
|
|
199
|
+
return EXIT_ERROR;
|
|
200
|
+
}
|
|
201
|
+
const diffs = diffManifests(expected, actual);
|
|
202
|
+
for (const d of diffs) {
|
|
203
|
+
process.stdout.write(`corpus_manifest: DIFF ${d.field}: expected ${d.expected}, got ${d.actual}\n`);
|
|
204
|
+
}
|
|
205
|
+
if (subjectsEquivalent(expected, actual)) {
|
|
206
|
+
process.stdout.write(
|
|
207
|
+
`corpus_manifest: SUBJECT EQUIVALENT · ${String(actual.included.length)} subject(s) · ` +
|
|
208
|
+
`digest=${actual.subject_digest.slice(0, 16)}` +
|
|
209
|
+
(diffs.length > 0 ? ` · ${String(diffs.length)} explanatory difference(s) above\n` : '\n'),
|
|
210
|
+
);
|
|
211
|
+
return EXIT_OK;
|
|
212
|
+
}
|
|
213
|
+
process.stdout.write(
|
|
214
|
+
'corpus_manifest: SUBJECT DIFFERS — a comparison captured against this manifest is not ' +
|
|
215
|
+
'comparable to one captured here\n',
|
|
216
|
+
);
|
|
217
|
+
return EXIT_SUBJECT_DIFFERS;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
process.stderr.write(`corpus_manifest: error: unknown verb ${JSON.stringify(parsed.verb)}\n${USAGE}`);
|
|
221
|
+
return EXIT_USAGE;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function _isCliEntry(): boolean {
|
|
225
|
+
try {
|
|
226
|
+
if (process.argv[1] === undefined) return false;
|
|
227
|
+
return fs.realpathSync(_HERE) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
228
|
+
} catch {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (_isCliEntry()) {
|
|
234
|
+
process.exit(main());
|
|
235
|
+
}
|