@event4u/agent-config 9.19.0 → 9.21.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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +89 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/{chunk-XGA2NTU4.js → chunk-3EWYC3YK.js} +40 -0
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +1 -1
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +1 -1
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +1 -1
- package/dist/hooks/dispatch.js +58 -2
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +47 -29
- package/package.json +1 -1
- package/src/config/gate-coverage.yml +38 -4
- package/src/config/gate-violation-baselines.json +3 -8
- package/src/scripts/_lib/gate_population.ts +70 -0
- package/src/scripts/_lib/roadmap_granularity.ts +157 -0
- package/src/scripts/_lib/scan_scope.ts +44 -0
- package/src/scripts/_lib/stdin.ts +82 -0
- package/src/scripts/audit_adr_coverage.ts +38 -0
- package/src/scripts/audit_auto_rules.ts +17 -3
- package/src/scripts/audit_command_surface.ts +51 -0
- package/src/scripts/audit_initial_context.ts +39 -3
- package/src/scripts/audit_mcp_tools.ts +25 -0
- package/src/scripts/audit_overlap.ts +24 -1
- package/src/scripts/audit_skill_descriptions.ts +19 -0
- package/src/scripts/{verify_before_complete_hook.ts → before_complete_hook.ts} +16 -3
- package/src/scripts/check_adr_frontmatter.ts +23 -3
- package/src/scripts/check_always_budget.ts +24 -0
- package/src/scripts/check_artefact_checksums.ts +24 -1
- package/src/scripts/check_artefact_count_messaging.ts +20 -0
- package/src/scripts/check_augmentignore.ts +22 -15
- package/src/scripts/check_backstop_debt.ts +22 -0
- package/src/scripts/check_beta_review_markers.ts +22 -1
- package/src/scripts/check_bridge_derivation.ts +22 -1
- package/src/scripts/check_bundle_path_leakage.ts +26 -0
- package/src/scripts/check_ci_strict_superset.ts +21 -3
- package/src/scripts/check_claims.ts +22 -0
- package/src/scripts/check_cli_registry_budget_sync.ts +33 -4
- package/src/scripts/check_cluster_patterns.ts +26 -4
- package/src/scripts/check_command_count_messaging.ts +20 -4
- package/src/scripts/check_comparison.ts +18 -0
- package/src/scripts/check_completion_review.ts +19 -4
- package/src/scripts/check_condensed_paths.ts +23 -2
- package/src/scripts/check_consumer_scope_flip.ts +21 -2
- package/src/scripts/check_council_config_location.ts +20 -0
- package/src/scripts/check_council_layout.ts +22 -2
- package/src/scripts/check_council_references.ts +23 -1
- package/src/scripts/check_dependency_floors.ts +19 -3
- package/src/scripts/check_discovery_determinism.ts +26 -0
- package/src/scripts/check_enforcement_coverage.ts +20 -0
- package/src/scripts/check_evaluator_budgets.ts +23 -0
- package/src/scripts/check_finding_dispositions.ts +29 -0
- package/src/scripts/check_gate_coverage.ts +63 -22
- package/src/scripts/check_gate_paths.ts +30 -5
- package/src/scripts/check_generated_artefact_headers.ts +21 -0
- package/src/scripts/check_generator_output_coverage.ts +23 -0
- package/src/scripts/check_gitignore_freshness.ts +23 -0
- package/src/scripts/check_host_loadability.ts +34 -8
- package/src/scripts/check_installer_import_purity.ts +22 -8
- package/src/scripts/check_kernel_prefix_stability.ts +19 -1
- package/src/scripts/check_kernel_rule_bundle.ts +23 -0
- package/src/scripts/check_knowledge_cards.ts +13 -0
- package/src/scripts/check_knowledge_pages.ts +23 -0
- package/src/scripts/check_knowledge_sharing.ts +27 -0
- package/src/scripts/check_md_language.ts +24 -0
- package/src/scripts/check_media_deps.ts +23 -0
- package/src/scripts/check_memory.ts +26 -1
- package/src/scripts/check_memory_contradiction.ts +16 -1
- package/src/scripts/check_memory_proposal.ts +15 -0
- package/src/scripts/check_memory_similarity.ts +16 -0
- package/src/scripts/check_module_management_neutral.ts +19 -11
- package/src/scripts/check_no_conflict_markers.ts +22 -1
- package/src/scripts/check_no_external_sources.ts +26 -1
- package/src/scripts/check_no_local_settings_committed.ts +37 -10
- package/src/scripts/check_no_new_legacy_path.ts +30 -1
- package/src/scripts/check_one_off_location.ts +43 -0
- package/src/scripts/check_originality_freshness.ts +25 -1
- package/src/scripts/check_overlay_cascade_subdirs.ts +21 -0
- package/src/scripts/check_pack_size.ts +22 -0
- package/src/scripts/check_preamble_payload_budget.ts +35 -5
- package/src/scripts/check_proposal.ts +24 -1
- package/src/scripts/check_public_catalog_links.ts +27 -0
- package/src/scripts/check_public_links.ts +19 -0
- package/src/scripts/check_quality_regression.ts +19 -0
- package/src/scripts/check_reach_channels.ts +44 -0
- package/src/scripts/check_reach_staleness.ts +27 -0
- package/src/scripts/check_release_adjacent_health.ts +28 -0
- package/src/scripts/check_release_highlights.ts +34 -0
- package/src/scripts/check_release_includes_discovery.ts +25 -6
- package/src/scripts/check_release_pr_shape.ts +18 -1
- package/src/scripts/check_release_published.ts +21 -0
- package/src/scripts/check_release_surface_equality.ts +22 -0
- package/src/scripts/check_release_trunk_sync.ts +19 -0
- package/src/scripts/check_reply_consistency.ts +22 -0
- package/src/scripts/check_review_dispositions.ts +188 -0
- package/src/scripts/check_roadmap_trackable.ts +20 -0
- package/src/scripts/check_role_doc_links.ts +23 -0
- package/src/scripts/check_rule_invariants.ts +14 -0
- package/src/scripts/check_secret_leak.ts +47 -0
- package/src/scripts/check_skill_gaps.ts +32 -0
- package/src/scripts/check_skill_requires.ts +19 -0
- package/src/scripts/check_source_pointer_freshness.ts +20 -0
- package/src/scripts/check_structural_breaking.ts +29 -2
- package/src/scripts/check_surface_tiers.ts +34 -0
- package/src/scripts/check_template_pin_drift.ts +23 -1
- package/src/scripts/check_test_coverage_diff.ts +17 -1
- package/src/scripts/check_token_optimizer_freshness.ts +18 -3
- package/src/scripts/check_token_quality_golden.ts +21 -0
- package/src/scripts/check_token_regression.ts +25 -0
- package/src/scripts/check_tracked_but_ignored.ts +26 -0
- package/src/scripts/check_trigger_eval_presence.ts +23 -1
- package/src/scripts/check_trigger_evals.ts +36 -0
- package/src/scripts/check_trunk_drift.ts +23 -0
- package/src/scripts/check_update_banner.ts +24 -0
- package/src/scripts/dispatch_r2_reviewer.ts +3 -0
- package/src/scripts/gate_scope_classify.ts +513 -0
- package/src/scripts/hook_manifest.yaml +1 -1
- package/src/scripts/hooks/block_kernel_rule_writes.ts +1 -1
- package/src/scripts/hooks/concern_registry.ts +2 -2
- package/src/scripts/lint_agent_security.ts +19 -0
- package/src/scripts/lint_agent_skill_names.ts +30 -0
- package/src/scripts/lint_agents_layout.ts +20 -0
- package/src/scripts/lint_agents_md.ts +20 -0
- package/src/scripts/lint_archived_skills.ts +24 -3
- package/src/scripts/lint_behavioural_eval_freshness.ts +29 -3
- package/src/scripts/lint_bench_ab.ts +25 -0
- package/src/scripts/lint_bench_corpus.ts +20 -4
- package/src/scripts/lint_breaking_changes_index.ts +24 -0
- package/src/scripts/lint_budget_ownership.ts +20 -3
- package/src/scripts/lint_changelog_rollback.ts +24 -0
- package/src/scripts/lint_command_flow_coverage.ts +22 -0
- package/src/scripts/lint_command_tiers.ts +29 -5
- package/src/scripts/lint_commit_subjects.ts +15 -0
- package/src/scripts/lint_confusables.ts +24 -0
- package/src/scripts/lint_consumer_internal_refs.ts +24 -0
- package/src/scripts/lint_design_quality.ts +22 -0
- package/src/scripts/lint_design_slop.ts +23 -0
- package/src/scripts/lint_discovery_manifest.ts +23 -0
- package/src/scripts/lint_discovery_vocabulary.ts +19 -0
- package/src/scripts/lint_documented_commands.ts +23 -0
- package/src/scripts/lint_empty_roadmaps.ts +23 -0
- package/src/scripts/lint_eval_fixture_citations.ts +19 -0
- package/src/scripts/lint_eval_freshness.ts +35 -0
- package/src/scripts/lint_examples.ts +18 -3
- package/src/scripts/lint_explain_trace.ts +23 -0
- package/src/scripts/lint_featured_skills.ts +26 -1
- package/src/scripts/lint_flows.ts +19 -0
- package/src/scripts/lint_frontmatter_boilerplate.ts +20 -0
- package/src/scripts/lint_ghostwriter_source.ts +26 -6
- package/src/scripts/lint_glama_drift.ts +24 -0
- package/src/scripts/lint_global_paths.ts +24 -0
- package/src/scripts/lint_governed_writes.ts +20 -0
- package/src/scripts/lint_hidden_unicode.ts +25 -0
- package/src/scripts/lint_hook_concern_budget.ts +39 -6
- package/src/scripts/lint_hook_manifest.ts +22 -0
- package/src/scripts/lint_instruction_smuggling.ts +23 -0
- package/src/scripts/lint_knowledge_scale.ts +32 -2
- package/src/scripts/lint_legal_pack.ts +31 -0
- package/src/scripts/lint_marketplace.ts +26 -0
- package/src/scripts/lint_marketplace_install_completeness.ts +23 -0
- package/src/scripts/lint_mcp_config_security.ts +24 -0
- package/src/scripts/lint_mcp_registry_manifest.ts +22 -1
- package/src/scripts/lint_memory_tripwire.ts +35 -0
- package/src/scripts/lint_missions.ts +23 -7
- package/src/scripts/lint_model_tier_coverage.ts +18 -0
- package/src/scripts/lint_namespace_collisions.ts +19 -0
- package/src/scripts/lint_no_new_atomic_commands.ts +33 -0
- package/src/scripts/lint_no_python_twin_rationale.ts +18 -0
- package/src/scripts/lint_one_off_age.ts +38 -2
- package/src/scripts/lint_orchestration_dsl.ts +23 -3
- package/src/scripts/lint_orchestrator_auto_detect.ts +19 -12
- package/src/scripts/lint_originality.ts +22 -4
- package/src/scripts/lint_originality_shingles.ts +28 -3
- package/src/scripts/lint_output_slop.ts +19 -0
- package/src/scripts/lint_override_kernel_guard.ts +30 -0
- package/src/scripts/lint_pack_first_win.ts +23 -0
- package/src/scripts/lint_pack_risk_class.ts +19 -3
- package/src/scripts/lint_persistence.ts +19 -0
- package/src/scripts/lint_persona_governance.ts +18 -3
- package/src/scripts/lint_positioning.ts +23 -0
- package/src/scripts/lint_pre_migration_refs.ts +24 -0
- package/src/scripts/lint_profile_overlay_set_only.ts +24 -0
- package/src/scripts/lint_provenance.ts +21 -0
- package/src/scripts/lint_provenance_vocabulary.ts +24 -1
- package/src/scripts/lint_readme_jargon.ts +21 -3
- package/src/scripts/lint_readme_serial_comma.ts +21 -3
- package/src/scripts/lint_readme_size.ts +21 -3
- package/src/scripts/lint_regression.ts +28 -0
- package/src/scripts/lint_roadmap_blockers.ts +43 -0
- package/src/scripts/lint_roadmap_ci_steps.ts +45 -0
- package/src/scripts/lint_roadmap_complexity.ts +47 -0
- package/src/scripts/lint_roadmap_later_disposition.ts +23 -0
- package/src/scripts/lint_rule_interactions.ts +16 -2
- package/src/scripts/lint_rule_tiers.ts +19 -6
- package/src/scripts/lint_showcase_sessions.ts +19 -3
- package/src/scripts/lint_skill_descriptions.ts +19 -0
- package/src/scripts/lint_skill_frontmatter_safety.ts +24 -0
- package/src/scripts/lint_skill_originality.ts +18 -3
- package/src/scripts/lint_skill_scripts_readonly.ts +20 -0
- package/src/scripts/lint_skill_tools.ts +84 -145
- package/src/scripts/lint_spawn_payload.ts +27 -1
- package/src/scripts/lint_store_boundary.ts +19 -0
- package/src/scripts/lint_subagent_determinism.ts +23 -1
- package/src/scripts/lint_supported_tools_matrix.ts +18 -0
- package/src/scripts/lint_surface_matrix.ts +19 -3
- package/src/scripts/lint_ticket_buildable.ts +23 -0
- package/src/scripts/lint_topics_yaml.ts +19 -12
- package/src/scripts/lint_trigger_precision.ts +27 -0
- package/src/scripts/lint_trust_coherence.ts +21 -0
- package/src/scripts/lint_ui_stack_bundles.ts +17 -3
- package/src/scripts/lint_value_dashboard.ts +19 -3
- package/src/scripts/lint_versioned_cache.ts +19 -1
- package/src/scripts/lint_workflow_security.ts +22 -14
- package/src/scripts/pr_url_reminder_hook.ts +1 -1
- package/src/scripts/skill_collision_clusters.ts +37 -3
- package/src/scripts/skill_discovery.ts +20 -0
- package/src/scripts/skill_eval_coverage.ts +22 -0
- package/src/scripts/skill_overlap.ts +32 -6
- package/src/scripts/skill_preview.ts +31 -0
- package/src/scripts/skill_trigger_eval.ts +18 -0
- package/src/scripts/skill_usage_collect.ts +21 -0
- package/src/scripts/skill_usage_report.ts +21 -0
- package/src/scripts/sweep_dead_scan_roots.ts +11 -3
- package/src/scripts/verify_physical_move.ts +24 -0
- package/src/scripts/check_bite_sized_granularity.ts +0 -116
- package/src/scripts/lint_workspace_boundary.ts +0 -267
|
@@ -45,6 +45,7 @@ import * as path from 'node:path';
|
|
|
45
45
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
46
46
|
|
|
47
47
|
import { artefact_roots } from './_lib/agent_src.js';
|
|
48
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
48
49
|
|
|
49
50
|
const _HERE = fileURLToPath(import.meta.url);
|
|
50
51
|
const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
@@ -269,8 +270,10 @@ export function cmd_scan_dir(root: string): number {
|
|
|
269
270
|
roots = [root];
|
|
270
271
|
}
|
|
271
272
|
const violations: Array<[string, number, string]> = [];
|
|
273
|
+
let scanned = 0;
|
|
272
274
|
for (const r of roots) {
|
|
273
275
|
for (const md of _rglobMdSorted(r)) {
|
|
276
|
+
scanned += 1;
|
|
274
277
|
const text = fs.readFileSync(md, 'utf-8');
|
|
275
278
|
const lines = _splitlines(text);
|
|
276
279
|
for (let idx = 0; idx < lines.length; idx++) {
|
|
@@ -282,6 +285,25 @@ export function cmd_scan_dir(root: string): number {
|
|
|
282
285
|
}
|
|
283
286
|
}
|
|
284
287
|
}
|
|
288
|
+
// The CI invocation points at the retired `.agent-src.uncondensed` root and
|
|
289
|
+
// relies on the fallback above; an EMPTY leftover of that directory still
|
|
290
|
+
// passes `_isDir`, so the fallback never fires and the sweep reads nothing.
|
|
291
|
+
try {
|
|
292
|
+
assertScanned({
|
|
293
|
+
gate: 'check_reply_consistency',
|
|
294
|
+
scanned,
|
|
295
|
+
units: 'markdown file(s)',
|
|
296
|
+
roots: roots.map((r) => _relToPosixOrAbs(r, ROOT)),
|
|
297
|
+
});
|
|
298
|
+
} catch (exc) {
|
|
299
|
+
if (exc instanceof DeadScopeError) {
|
|
300
|
+
// 1 = usage/IO error, as for the unresolvable-root case above
|
|
301
|
+
// (2 is reserved for findings).
|
|
302
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
303
|
+
return 1;
|
|
304
|
+
}
|
|
305
|
+
throw exc;
|
|
306
|
+
}
|
|
285
307
|
if (violations.length > 0) {
|
|
286
308
|
for (const [p, line, snippet] of violations) {
|
|
287
309
|
process.stderr.write(` 🔴 ${p}:${String(line)} — inline-tag — ${snippet}\n`);
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Archived-review disposition check (contract §2.7 + §2.2).
|
|
4
|
+
*
|
|
5
|
+
* Gate R2 selects only artefacts relevant to the CURRENT review scope (§2.6),
|
|
6
|
+
* and superseded rounds are renamed out of the `*.findings.md` glob on purpose.
|
|
7
|
+
* The consequence nobody noticed until it bit: **no validator reads archived
|
|
8
|
+
* round records at all**, so a finding left `open` after it was fixed is caught
|
|
9
|
+
* by nothing. That is exactly how two fixed findings sat recorded as `open` for
|
|
10
|
+
* a day in this repo's own corpus.
|
|
11
|
+
*
|
|
12
|
+
* This closes that gap with the smallest mechanism that covers the observed
|
|
13
|
+
* failure: archiving a round asserts the round is CLOSED, so every row in an
|
|
14
|
+
* archived record must carry a terminal status and a non-empty Reason/Ref.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately NOT the disposition index with stable ids that was originally
|
|
17
|
+
* proposed. That design assumed dispositions live OUTSIDE the round records;
|
|
18
|
+
* measured across the corpus, they do not — records are terminal in place. An
|
|
19
|
+
* index would be a second artefact to keep in sync, and therefore a new drift
|
|
20
|
+
* source, guarding a failure mode that has not occurred. Trigger to revisit: a
|
|
21
|
+
* disposition that genuinely cannot be recorded in the round record itself.
|
|
22
|
+
*
|
|
23
|
+
* Scope: `*-review.md` under the reviews root, excluding `*.findings.md` (those
|
|
24
|
+
* are live artefacts and belong to `check_completion_review`).
|
|
25
|
+
*
|
|
26
|
+
* Reference SHAPE is deliberately NOT checked, and that was a measurement, not
|
|
27
|
+
* a preference. The first draft resolved a `fixed` ref via `rev-parse` and a
|
|
28
|
+
* `deferred` ref via a path probe. Run against the real corpus it produced 8
|
|
29
|
+
* blocks — every one of them a pre-existing archived record whose reference is
|
|
30
|
+
* prose describing the change, or a carrier named by bare slug. None of the 8
|
|
31
|
+
* was the failure this gate exists to catch. Archived records are frozen
|
|
32
|
+
* (§2.7), so a shape rule cannot be satisfied retroactively without editing
|
|
33
|
+
* them; a gate whose only output is unfixable blocks is the gate that gets
|
|
34
|
+
* switched off. Trigger to revisit: a review round where an unresolvable
|
|
35
|
+
* reference actually hid a disposition, on a record written AFTER this gate
|
|
36
|
+
* shipped and therefore fixable at source.
|
|
37
|
+
*
|
|
38
|
+
* Exit codes (contract §6): 0 = pass, 1 = policy violation (including a DEAD
|
|
39
|
+
* SCAN SCOPE — a gate that read nothing has not passed), 2 = internal error
|
|
40
|
+
* (the CALLER applies degraded advisory mode). `scanned:` is emitted on EVERY
|
|
41
|
+
* exit path, exit 2 included.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import * as fs from 'node:fs';
|
|
45
|
+
import * as path from 'node:path';
|
|
46
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
47
|
+
|
|
48
|
+
import { DeadScopeError, assertScanned } from './_lib/scan_scope.js';
|
|
49
|
+
import { parseArtifact } from './check_completion_review.js';
|
|
50
|
+
|
|
51
|
+
const REVIEWS_ROOT = 'agents/evidence/reviews';
|
|
52
|
+
const TERMINAL = new Set(['fixed', 'accepted-risk', 'deferred']);
|
|
53
|
+
|
|
54
|
+
export interface Violation {
|
|
55
|
+
kind: string;
|
|
56
|
+
file: string;
|
|
57
|
+
detail: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** An archived record: a `*-review.md` that is not a live `*.findings.md`. */
|
|
61
|
+
export function isArchivedRecord(rel: string): boolean {
|
|
62
|
+
const base = rel.replace(/\\/g, '/').split('/').pop() ?? '';
|
|
63
|
+
return base.endsWith('-review.md') && !base.endsWith('.findings.md');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface CheckOptions {
|
|
67
|
+
repo: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Contract §2.7: every row of an archived record is terminal, with its reference. */
|
|
71
|
+
export function checkRecord(rel: string, text: string, _opts: CheckOptions): Violation[] {
|
|
72
|
+
const violations: Violation[] = [];
|
|
73
|
+
const parsed = parseArtifact(text);
|
|
74
|
+
for (const row of parsed.rows) {
|
|
75
|
+
const where = `row ${row.index} (line ${String(row.line)})`;
|
|
76
|
+
if (row.status === 'open') {
|
|
77
|
+
violations.push({
|
|
78
|
+
kind: 'open-in-archived-record',
|
|
79
|
+
file: rel,
|
|
80
|
+
detail:
|
|
81
|
+
`${where}: status \`open\` in an ARCHIVED record. Renaming a round out of the ` +
|
|
82
|
+
'`*.findings.md` glob asserts it is closed, so every row must already be terminal ' +
|
|
83
|
+
'(`fixed` / `accepted-risk` / `deferred`) — contract §2.7.',
|
|
84
|
+
});
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (!TERMINAL.has(row.status)) {
|
|
88
|
+
violations.push({
|
|
89
|
+
kind: 'bad-status',
|
|
90
|
+
file: rel,
|
|
91
|
+
detail: `${where}: unknown status \`${row.status}\` — expected fixed / accepted-risk / deferred.`,
|
|
92
|
+
});
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (row.reasonRef.trim() === '') {
|
|
96
|
+
violations.push({
|
|
97
|
+
kind: 'missing-reference',
|
|
98
|
+
file: rel,
|
|
99
|
+
detail: `${where}: status \`${row.status}\` requires a Reason/Ref, which is empty.`,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return violations;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function main(argv?: readonly string[]): number {
|
|
107
|
+
const args = argv ?? process.argv.slice(2);
|
|
108
|
+
let repo = process.cwd();
|
|
109
|
+
let root = REVIEWS_ROOT;
|
|
110
|
+
let quiet = false;
|
|
111
|
+
for (let i = 0; i < args.length; i++) {
|
|
112
|
+
const a = args[i] as string;
|
|
113
|
+
if (a === '--repo') repo = args[++i] as string;
|
|
114
|
+
else if (a === '--reviews-root') root = args[++i] as string;
|
|
115
|
+
else if (a === '--quiet') quiet = true;
|
|
116
|
+
else if (a === '-h' || a === '--help') {
|
|
117
|
+
process.stdout.write('usage: check_review_dispositions [--repo PATH] [--reviews-root PATH] [--quiet]\n');
|
|
118
|
+
process.stdout.write('scanned: 0\n');
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const abs = path.resolve(repo, root);
|
|
124
|
+
let files: string[] = [];
|
|
125
|
+
try {
|
|
126
|
+
files = fs
|
|
127
|
+
.readdirSync(abs, { withFileTypes: true })
|
|
128
|
+
.filter((e) => e.isFile() && isArchivedRecord(e.name))
|
|
129
|
+
.map((e) => path.join(root, e.name))
|
|
130
|
+
.sort();
|
|
131
|
+
} catch {
|
|
132
|
+
// Absent root: report it as a DEAD SCOPE below rather than a clean pass.
|
|
133
|
+
files = [];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
assertScanned({
|
|
138
|
+
gate: 'check_review_dispositions',
|
|
139
|
+
scanned: files.length,
|
|
140
|
+
units: 'archived review record(s)',
|
|
141
|
+
roots: [root],
|
|
142
|
+
});
|
|
143
|
+
} catch (exc) {
|
|
144
|
+
// A dead scan scope is a POLICY violation (exit 1), never an internal
|
|
145
|
+
// error: exit 2 is warn-and-allow at every call site, so a moved reviews
|
|
146
|
+
// root would silently degrade this gate to advisory.
|
|
147
|
+
process.stdout.write('scanned: 0\n');
|
|
148
|
+
process.stderr.write(`❌ ${exc instanceof DeadScopeError ? exc.message : String(exc)}\n`);
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const violations: Violation[] = [];
|
|
153
|
+
let scanned = 0;
|
|
154
|
+
try {
|
|
155
|
+
for (const rel of files) {
|
|
156
|
+
const text = fs.readFileSync(path.resolve(repo, rel), 'utf-8');
|
|
157
|
+
scanned += 1;
|
|
158
|
+
violations.push(...checkRecord(rel, text, { repo }));
|
|
159
|
+
}
|
|
160
|
+
} catch (exc) {
|
|
161
|
+
process.stdout.write(`scanned: ${String(scanned)}\n`);
|
|
162
|
+
process.stderr.write(
|
|
163
|
+
`❌ check_review_dispositions: internal error after ${String(scanned)} record(s): ` +
|
|
164
|
+
`${exc instanceof Error ? exc.message : String(exc)}\n`,
|
|
165
|
+
);
|
|
166
|
+
return 2;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (violations.length > 0) {
|
|
170
|
+
process.stdout.write(`❌ ${String(violations.length)} disposition violation(s):\n\n`);
|
|
171
|
+
for (const v of violations) {
|
|
172
|
+
process.stdout.write(` ${v.kind} — ${v.file}\n │ ${v.detail}\n`);
|
|
173
|
+
}
|
|
174
|
+
} else if (!quiet) {
|
|
175
|
+
process.stdout.write(`✅ Archived review records all terminal (${String(scanned)} record(s) scanned).\n`);
|
|
176
|
+
}
|
|
177
|
+
process.stdout.write(`scanned: ${String(scanned)}\n`);
|
|
178
|
+
return violations.length > 0 ? 1 : 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
182
|
+
function _isCliEntry(): boolean {
|
|
183
|
+
if (process.argv[1] === undefined) return false;
|
|
184
|
+
return import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href || process.argv[1] === _HERE;
|
|
185
|
+
}
|
|
186
|
+
if (_isCliEntry()) {
|
|
187
|
+
process.exit(main());
|
|
188
|
+
}
|
|
@@ -29,6 +29,8 @@ import * as fs from 'node:fs';
|
|
|
29
29
|
import * as path from 'node:path';
|
|
30
30
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
31
31
|
|
|
32
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
33
|
+
|
|
32
34
|
const QUIET = process.argv.slice(2).includes('--quiet');
|
|
33
35
|
|
|
34
36
|
const ROADMAP_ROOT = 'agents/roadmaps';
|
|
@@ -224,6 +226,24 @@ function main(): number {
|
|
|
224
226
|
process.stderr.write(`❌ ${ROADMAP_ROOT} not found — run from project root.\n`);
|
|
225
227
|
return 1;
|
|
226
228
|
}
|
|
229
|
+
// The dir check above cannot see an existing-but-empty tree, and the unit
|
|
230
|
+
// has to be the unfiltered walk: "0 active roadmaps" is a real success
|
|
231
|
+
// state (everything archived), while 0 `.md` under the root never is.
|
|
232
|
+
try {
|
|
233
|
+
assertScanned({
|
|
234
|
+
gate: 'check_roadmap_trackable',
|
|
235
|
+
scanned: _rglobMdSorted(ROADMAP_ROOT).length,
|
|
236
|
+
units: 'markdown file(s)',
|
|
237
|
+
roots: [ROADMAP_ROOT],
|
|
238
|
+
});
|
|
239
|
+
} catch (exc) {
|
|
240
|
+
if (exc instanceof DeadScopeError) {
|
|
241
|
+
// 1 is this gate's only failure code (0 clean / 1 violation).
|
|
242
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
243
|
+
return 1;
|
|
244
|
+
}
|
|
245
|
+
throw exc;
|
|
246
|
+
}
|
|
227
247
|
const findings: string[] = [];
|
|
228
248
|
for (const p of find_active_roadmaps(ROADMAP_ROOT)) {
|
|
229
249
|
findings.push(...violations_for(p));
|
|
@@ -17,6 +17,8 @@ import * as fs from 'node:fs';
|
|
|
17
17
|
import * as path from 'node:path';
|
|
18
18
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
19
19
|
|
|
20
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
21
|
+
|
|
20
22
|
const _HERE = fileURLToPath(import.meta.url);
|
|
21
23
|
const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
22
24
|
const DOCS_DIR = path.join(ROOT, 'docs');
|
|
@@ -159,6 +161,27 @@ function main(argv?: readonly string[]): number {
|
|
|
159
161
|
}
|
|
160
162
|
}
|
|
161
163
|
|
|
164
|
+
// `scan` exits 2 when a role doc is missing outright, but a doc that still
|
|
165
|
+
// exists and has lost every internal link — rewritten to external URLs, or
|
|
166
|
+
// the how-to section moved elsewhere — leaves `checked` at 0 and prints
|
|
167
|
+
// "0 links OK across 2 files", which is indistinguishable from a pass.
|
|
168
|
+
try {
|
|
169
|
+
assertScanned({
|
|
170
|
+
gate: 'check_role_doc_links',
|
|
171
|
+
scanned: checked,
|
|
172
|
+
units: 'internal link(s)',
|
|
173
|
+
roots: ROLE_DOCS.map((d) => _relTo(d)),
|
|
174
|
+
});
|
|
175
|
+
} catch (exc) {
|
|
176
|
+
if (exc instanceof DeadScopeError) {
|
|
177
|
+
// 2 is this gate's existing scope-failure code (missing role doc);
|
|
178
|
+
// 1 means "broken links", which is not what happened here.
|
|
179
|
+
process.stderr.write(`${exc.message}\n`);
|
|
180
|
+
return 2;
|
|
181
|
+
}
|
|
182
|
+
throw exc;
|
|
183
|
+
}
|
|
184
|
+
|
|
162
185
|
if (failures.length > 0) {
|
|
163
186
|
process.stderr.write('Broken links in role docs:\n');
|
|
164
187
|
for (const [doc, line_no, url] of failures) {
|
|
@@ -33,6 +33,8 @@ import * as fs from 'node:fs';
|
|
|
33
33
|
import * as path from 'node:path';
|
|
34
34
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
35
35
|
|
|
36
|
+
import { assertWatchlistResolves } from './_lib/scan_scope.js';
|
|
37
|
+
|
|
36
38
|
// src/scripts/check_rule_invariants.ts → two dirs up is the repo root.
|
|
37
39
|
const _HERE = fileURLToPath(import.meta.url);
|
|
38
40
|
const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
@@ -247,6 +249,18 @@ function main(): number {
|
|
|
247
249
|
return mutationSelftest(args.root);
|
|
248
250
|
}
|
|
249
251
|
const entries = loadEntries(args.root);
|
|
252
|
+
// This guard has no corpus to walk — it byte-pins fragments of named
|
|
253
|
+
// kernel rules, so its scope IS that watch list. An emptied
|
|
254
|
+
// invariants.json yields no candidates at all and this prints
|
|
255
|
+
// "All 0 kernel rule invariants present in src + dist"; a wholesale
|
|
256
|
+
// move of `src/rules/` + `dist/agent-src/rules/` leaves it watching
|
|
257
|
+
// phantoms. DeadScopeError lands in the catch below → stderr + exit 3,
|
|
258
|
+
// the could-not-run code; 1 means a clause was genuinely lost.
|
|
259
|
+
assertWatchlistResolves({
|
|
260
|
+
gate: 'check_rule_invariants',
|
|
261
|
+
candidates: entries.flatMap((e) => [e.file, e.dist]),
|
|
262
|
+
repoRoot: args.root,
|
|
263
|
+
});
|
|
250
264
|
const invariantCount = entries.reduce((n, e) => n + e.strings.length, 0);
|
|
251
265
|
const misses = scan(args.root);
|
|
252
266
|
process.stdout.write(formatText(misses, invariantCount) + '\n');
|
|
@@ -37,6 +37,7 @@ import * as fs from 'node:fs';
|
|
|
37
37
|
import * as path from 'node:path';
|
|
38
38
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
39
39
|
|
|
40
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
40
41
|
import { scanText, type SecretFinding } from './_lib/secret_detector.js';
|
|
41
42
|
|
|
42
43
|
const _HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -253,6 +254,52 @@ function _stderr(s: string): void {
|
|
|
253
254
|
export function main(argv?: readonly string[]): number {
|
|
254
255
|
const args = parseArgs(argv ?? process.argv.slice(2));
|
|
255
256
|
const mode: Mode = args.paths.length > 0 ? 'explicit' : args.all ? 'all' : 'diff';
|
|
257
|
+
|
|
258
|
+
// Scope assertion, deliberately here and not inside `scanRepo`: that export
|
|
259
|
+
// is called directly by tests and other callers against arbitrary roots, and
|
|
260
|
+
// a secret gate is the last place to widen a throw. The cost is one extra
|
|
261
|
+
// `resolveFiles` (a `git ls-files` / `git diff`); the file reads are not
|
|
262
|
+
// repeated. Assert the RESOLVED set, before the exclude/binary filters —
|
|
263
|
+
// those legitimately drop everything (a diff touching only `tests/`), so
|
|
264
|
+
// filtering to zero is not the same signal as resolving to zero.
|
|
265
|
+
// Exit 2 (usage/env) over 1 (leak found) — a dead scope means the gate could
|
|
266
|
+
// not run. `resolveFiles` already hard-throws on an unresolvable base, so
|
|
267
|
+
// that case never arrives here as a quiet zero; a non-DeadScopeError is
|
|
268
|
+
// re-thrown untouched to preserve it.
|
|
269
|
+
try {
|
|
270
|
+
const targets = resolveFiles(REPO_ROOT, mode, { explicit: args.paths, base: args.base });
|
|
271
|
+
assertScanned({
|
|
272
|
+
gate: 'check_secret_leak',
|
|
273
|
+
scanned: targets.length,
|
|
274
|
+
units: 'candidate file(s)',
|
|
275
|
+
roots:
|
|
276
|
+
mode === 'all'
|
|
277
|
+
? ['git ls-files']
|
|
278
|
+
: mode === 'explicit'
|
|
279
|
+
? ['<explicit path arguments>']
|
|
280
|
+
: [`git diff --name-only --diff-filter=ACMR ${args.base ?? 'origin/main'}`, 'git ls-files --others --exclude-standard'],
|
|
281
|
+
// `all` and `explicit` carry no reason: zero tracked files, or zero
|
|
282
|
+
// named paths, is blindness in a repo that has content.
|
|
283
|
+
...(mode === 'diff'
|
|
284
|
+
? {
|
|
285
|
+
allowEmpty:
|
|
286
|
+
'EMPTY_VALID: shift-left mode scans what the change introduces, and a ' +
|
|
287
|
+
'branch identical to its base with no untracked files introduces nothing ' +
|
|
288
|
+
'— an absent question, not an empty corpus. Deletion test: wiping the ' +
|
|
289
|
+
'tracked tree would show up here as changed paths, and an unresolvable ' +
|
|
290
|
+
'base (the dangerous zero, which shallow CI produces) already throws in ' +
|
|
291
|
+
'resolveFiles above rather than reaching this line.',
|
|
292
|
+
}
|
|
293
|
+
: {}),
|
|
294
|
+
});
|
|
295
|
+
} catch (exc) {
|
|
296
|
+
if (exc instanceof DeadScopeError) {
|
|
297
|
+
_stderr(`❌ ${exc.message}\n`);
|
|
298
|
+
return 2;
|
|
299
|
+
}
|
|
300
|
+
throw exc;
|
|
301
|
+
}
|
|
302
|
+
|
|
256
303
|
const hits = scanRepo(REPO_ROOT, mode, { explicit: args.paths, base: args.base });
|
|
257
304
|
|
|
258
305
|
if (args.json) {
|
|
@@ -16,6 +16,8 @@ import * as path from 'node:path';
|
|
|
16
16
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
17
17
|
import { parse as parseYaml } from 'yaml';
|
|
18
18
|
|
|
19
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
20
|
+
|
|
19
21
|
const _HERE = fileURLToPath(import.meta.url);
|
|
20
22
|
export const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
21
23
|
const SKILLS_DIR = path.join(ROOT, 'src', 'skills');
|
|
@@ -76,8 +78,38 @@ export function findBrokenPointers(entries: SkillGaps[], root: string = ROOT): s
|
|
|
76
78
|
return errs;
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
/** SKILL.md files under `skillsDir` — the corpus `collectSkillGaps` reads. */
|
|
82
|
+
function countSkillFiles(skillsDir: string = SKILLS_DIR): number {
|
|
83
|
+
try {
|
|
84
|
+
return fs
|
|
85
|
+
.readdirSync(skillsDir)
|
|
86
|
+
.filter((name) => fs.existsSync(path.join(skillsDir, name, 'SKILL.md'))).length;
|
|
87
|
+
} catch {
|
|
88
|
+
// A missing root counts zero, which the caller's assertion turns into a
|
|
89
|
+
// loud failure — never a silent "no gaps to check".
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
79
94
|
export function main(argv: string[] = process.argv.slice(2)): number {
|
|
80
95
|
const quiet = argv.includes('--quiet');
|
|
96
|
+
// The scanned unit is every SKILL.md, not the subset declaring `gaps:` —
|
|
97
|
+
// asserting on the declaration count would read a vanished skills tree as
|
|
98
|
+
// "no gaps declared" and stay green, the exact blindness this guards.
|
|
99
|
+
try {
|
|
100
|
+
assertScanned({
|
|
101
|
+
gate: 'check_skill_gaps',
|
|
102
|
+
scanned: countSkillFiles(),
|
|
103
|
+
units: 'SKILL.md file(s)',
|
|
104
|
+
roots: ['src/skills'],
|
|
105
|
+
});
|
|
106
|
+
} catch (exc) {
|
|
107
|
+
if (exc instanceof DeadScopeError) {
|
|
108
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
109
|
+
return 1;
|
|
110
|
+
}
|
|
111
|
+
throw exc;
|
|
112
|
+
}
|
|
81
113
|
const entries = collectSkillGaps();
|
|
82
114
|
const errs = findBrokenPointers(entries);
|
|
83
115
|
if (errs.length > 0) {
|
|
@@ -19,6 +19,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
19
19
|
import YAML from 'yaml';
|
|
20
20
|
|
|
21
21
|
import { ROOT, iter_artefacts } from './_lib/agent_src.js';
|
|
22
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
22
23
|
import { parse_frontmatter } from './validate_frontmatter.js';
|
|
23
24
|
|
|
24
25
|
const _HERE = fileURLToPath(import.meta.url);
|
|
@@ -107,6 +108,24 @@ function _set_hooks_for_test(overrides: Partial<typeof _hooks>): void {
|
|
|
107
108
|
function main(): number {
|
|
108
109
|
const closure = _hooks.load_pack_closure();
|
|
109
110
|
const skills = _hooks.collect_skills();
|
|
111
|
+
// The unit is every parsed SKILL.md, not the subset declaring
|
|
112
|
+
// `requires_skills:` — over a vanished skills tree "0 composition edges"
|
|
113
|
+
// is indistinguishable from a suite that legitimately declares none.
|
|
114
|
+
try {
|
|
115
|
+
assertScanned({
|
|
116
|
+
gate: 'check_skill_requires',
|
|
117
|
+
scanned: Object.keys(skills).length,
|
|
118
|
+
units: 'SKILL.md file(s)',
|
|
119
|
+
roots: ['src/agent-src', 'src/skills', 'src/rules'],
|
|
120
|
+
});
|
|
121
|
+
} catch (exc) {
|
|
122
|
+
if (exc instanceof DeadScopeError) {
|
|
123
|
+
// 1 is this gate's only failure code (0 clean / 1 violations).
|
|
124
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
125
|
+
return 1;
|
|
126
|
+
}
|
|
127
|
+
throw exc;
|
|
128
|
+
}
|
|
110
129
|
const errors: string[] = [];
|
|
111
130
|
|
|
112
131
|
for (const skill_id of Object.keys(skills).sort()) {
|
|
@@ -36,6 +36,8 @@ import * as path from 'node:path';
|
|
|
36
36
|
import process from 'node:process';
|
|
37
37
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
38
38
|
|
|
39
|
+
import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
|
|
40
|
+
|
|
39
41
|
const _HERE = fileURLToPath(import.meta.url);
|
|
40
42
|
// parents[2] of src/scripts/<file> is the repo root.
|
|
41
43
|
const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
@@ -142,6 +144,24 @@ function main(argv: readonly string[]): number {
|
|
|
142
144
|
return _selftest();
|
|
143
145
|
}
|
|
144
146
|
const asJson = argv.includes('--json');
|
|
147
|
+
// Scope is the allowlist itself — there is no tree to walk. An emptied
|
|
148
|
+
// AUTHORING_FILES scans nothing and reports "No retired source-of-truth
|
|
149
|
+
// pointers" forever, which is the one result this gate must never produce
|
|
150
|
+
// by accident. Exit 2 (the usage / self-test class, i.e. the gate could
|
|
151
|
+
// not run), never 1 — 1 asserts a retired pointer was actually found.
|
|
152
|
+
try {
|
|
153
|
+
assertWatchlistResolves({
|
|
154
|
+
gate: 'check_source_pointer_freshness',
|
|
155
|
+
candidates: AUTHORING_FILES,
|
|
156
|
+
repoRoot: ROOT,
|
|
157
|
+
});
|
|
158
|
+
} catch (err) {
|
|
159
|
+
if (err instanceof DeadScopeError) {
|
|
160
|
+
process.stderr.write(`❌ ${err.message}\n`);
|
|
161
|
+
return 2;
|
|
162
|
+
}
|
|
163
|
+
throw err;
|
|
164
|
+
}
|
|
145
165
|
const hits: Hit[] = [];
|
|
146
166
|
for (const rel of AUTHORING_FILES) {
|
|
147
167
|
hits.push(..._scanFile(rel));
|
|
@@ -30,6 +30,8 @@ import * as path from 'node:path';
|
|
|
30
30
|
import * as fs from 'node:fs';
|
|
31
31
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
32
32
|
|
|
33
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
34
|
+
|
|
33
35
|
const QUIET = process.argv.includes('--quiet');
|
|
34
36
|
// src/scripts/check_structural_breaking.ts → three levels up is the repo root
|
|
35
37
|
// (Python: Path(__file__).resolve().parent.parent.parent).
|
|
@@ -91,12 +93,37 @@ function main(): number {
|
|
|
91
93
|
return 0; // no trunk to diff against (e.g. shallow CI) → no-op
|
|
92
94
|
}
|
|
93
95
|
const names = _git('diff', '--name-status', `${base}...HEAD`);
|
|
94
|
-
|
|
96
|
+
const changed = _splitlines(names);
|
|
97
|
+
// Replaces the bare `names.trim() === ''` early return: a branch identical
|
|
98
|
+
// to trunk genuinely changes nothing, so zero is allowed — but it now says
|
|
99
|
+
// so through the shared primitive instead of a silent `return 0`.
|
|
100
|
+
// Exit 1 is this pinned CLI's only failure code.
|
|
101
|
+
try {
|
|
102
|
+
assertScanned({
|
|
103
|
+
gate: 'check_structural_breaking',
|
|
104
|
+
scanned: changed.length,
|
|
105
|
+
units: 'changed path(s)',
|
|
106
|
+
roots: [`git diff --name-status ${BASE_REF}...HEAD`],
|
|
107
|
+
allowEmpty:
|
|
108
|
+
'EMPTY_VALID: a branch with no diff against trunk has no structural change to ' +
|
|
109
|
+
'judge — an absent question, not an empty corpus. Deletion test: deleting the ' +
|
|
110
|
+
'watched artifact trees would show up here as D lines (the very break this gate ' +
|
|
111
|
+
'reports), so a wiped tree cannot produce this zero. An unresolvable base does ' +
|
|
112
|
+
'not reach here either — it exits at the no-trunk no-op above.',
|
|
113
|
+
});
|
|
114
|
+
} catch (exc) {
|
|
115
|
+
if (exc instanceof DeadScopeError) {
|
|
116
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
throw exc;
|
|
120
|
+
}
|
|
121
|
+
if (changed.length === 0) {
|
|
95
122
|
return 0; // nothing changed vs trunk
|
|
96
123
|
}
|
|
97
124
|
|
|
98
125
|
const breaks: string[] = [];
|
|
99
|
-
for (const line of
|
|
126
|
+
for (const line of changed) {
|
|
100
127
|
const parts = line.split('\t');
|
|
101
128
|
const status = parts[0]!;
|
|
102
129
|
if (status.startsWith('D') && ARTIFACT_RE.test(parts[1]!)) {
|
|
@@ -37,6 +37,8 @@ import * as path from 'node:path';
|
|
|
37
37
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
38
38
|
import { parse as parseYaml } from 'yaml';
|
|
39
39
|
|
|
40
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
41
|
+
|
|
40
42
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
41
43
|
const SCRIPTS = path.join(ROOT, 'src', 'scripts');
|
|
42
44
|
const REGISTRY = path.join(SCRIPTS, 'surface-tiers.yml');
|
|
@@ -105,6 +107,20 @@ function check_exhaustive(clusters: Map<string, string>): string[] {
|
|
|
105
107
|
return errors;
|
|
106
108
|
}
|
|
107
109
|
|
|
110
|
+
/** Cluster dirs on disk under SCRIPTS — the corpus `check_exhaustive` classifies. */
|
|
111
|
+
function _count_cluster_dirs(): number {
|
|
112
|
+
let children: fs.Dirent[];
|
|
113
|
+
try {
|
|
114
|
+
children = fs.readdirSync(SCRIPTS, { withFileTypes: true });
|
|
115
|
+
} catch {
|
|
116
|
+
// A missing root counts zero; the caller's assertion makes that loud.
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
return children.filter(
|
|
120
|
+
(c) => !_IGNORE_DIRS.has(c.name) && _isDir(path.join(SCRIPTS, c.name)),
|
|
121
|
+
).length;
|
|
122
|
+
}
|
|
123
|
+
|
|
108
124
|
function _module_tier(
|
|
109
125
|
dotted: string,
|
|
110
126
|
clusters: Map<string, string>,
|
|
@@ -527,6 +543,24 @@ function main(argv?: readonly string[]): number {
|
|
|
527
543
|
args.skip_imports || envSkip === '1' || envSkip === 'true' || envSkip === 'yes';
|
|
528
544
|
|
|
529
545
|
const { clusters, lab_modules } = _load_registry();
|
|
546
|
+
// The unit is the cluster dirs found on disk, not `clusters.size` (the
|
|
547
|
+
// registry's own entries): over a vanished src/scripts/ every registry
|
|
548
|
+
// entry goes unchallenged and both assertions are trivially clean.
|
|
549
|
+
try {
|
|
550
|
+
assertScanned({
|
|
551
|
+
gate: 'check_surface_tiers',
|
|
552
|
+
scanned: _count_cluster_dirs(),
|
|
553
|
+
units: 'cluster dir(s)',
|
|
554
|
+
roots: ['src/scripts'],
|
|
555
|
+
});
|
|
556
|
+
} catch (exc) {
|
|
557
|
+
if (exc instanceof DeadScopeError) {
|
|
558
|
+
// 1 = violation(s); 2 is reserved for argparse usage errors.
|
|
559
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
560
|
+
return 1;
|
|
561
|
+
}
|
|
562
|
+
throw exc;
|
|
563
|
+
}
|
|
530
564
|
const errors = check_exhaustive(clusters);
|
|
531
565
|
if (skip_imports) {
|
|
532
566
|
process.stdout.write('surface-tiers: import boundary check SKIPPED (kill-switch).\n');
|
|
@@ -27,6 +27,7 @@ import * as path from 'node:path';
|
|
|
27
27
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
28
28
|
|
|
29
29
|
import { resolve_logical } from './_lib/agent_src.js';
|
|
30
|
+
import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
|
|
30
31
|
|
|
31
32
|
const _HERE = fileURLToPath(import.meta.url);
|
|
32
33
|
// Python `Path(__file__).resolve().parents[2]` — two dirs up from src/scripts.
|
|
@@ -159,8 +160,29 @@ function main(argv: readonly string[] = []): number {
|
|
|
159
160
|
return 1;
|
|
160
161
|
}
|
|
161
162
|
|
|
163
|
+
// Scope is two named templates, one of them resolved through
|
|
164
|
+
// `resolve_logical` — a resolver returning a path under an artefact root
|
|
165
|
+
// that no longer exists is exactly how this comparison ends up with
|
|
166
|
+
// nothing to compare. Exit 1: it is this gate's only failure code, and it
|
|
167
|
+
// already carries the could-not-run meaning (a missing template file is
|
|
168
|
+
// reported as a failure below); 2 is reserved for CLI misuse.
|
|
169
|
+
const templates = _template_files();
|
|
170
|
+
try {
|
|
171
|
+
assertWatchlistResolves({
|
|
172
|
+
gate: 'check_template_pin_drift',
|
|
173
|
+
candidates: templates.map((p) => path.relative(REPO_ROOT, p)),
|
|
174
|
+
repoRoot: REPO_ROOT,
|
|
175
|
+
});
|
|
176
|
+
} catch (exc) {
|
|
177
|
+
if (exc instanceof DeadScopeError) {
|
|
178
|
+
process.stderr.write(`❌ check_template_pin_drift: ${exc.message}\n`);
|
|
179
|
+
return 1;
|
|
180
|
+
}
|
|
181
|
+
throw exc;
|
|
182
|
+
}
|
|
183
|
+
|
|
162
184
|
const failures: string[] = [];
|
|
163
|
-
for (const template of
|
|
185
|
+
for (const template of templates) {
|
|
164
186
|
const rel = _relToPosixOrAbs(template, REPO_ROOT);
|
|
165
187
|
if (!_isFile(template)) {
|
|
166
188
|
failures.push(`missing template file: ${rel}`);
|