@event4u/agent-config 16.0.0 → 16.1.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 +297 -137
- package/CONTRIBUTING.md +9 -0
- package/README.md +2 -2
- package/dist/agent-src/commands/analyze/inbox.md +26 -0
- package/dist/agent-src/commands/analyze/roadmap-repos.md +27 -0
- package/dist/agent-src/commands/challenge-me/closure.md +178 -0
- package/dist/agent-src/commands/challenge-me.md +9 -2
- package/dist/agent-src/commands/council/default.md +25 -5
- package/dist/agent-src/commands/feature/plan.md +28 -1
- package/dist/agent-src/commands/feature/roadmap.md +28 -1
- package/dist/agent-src/commands/implement-ticket.md +27 -0
- package/dist/agent-src/commands/jira-ticket.md +27 -0
- package/dist/agent-src/commands/pr/create.md +29 -0
- package/dist/agent-src/commands/pr/merge.md +60 -9
- package/dist/agent-src/commands/roadmap/ai-council.md +7 -1
- package/dist/agent-src/commands/roadmap/create.md +74 -27
- package/dist/agent-src/commands/roadmap/materialize.md +27 -0
- package/dist/agent-src/commands/roadmap/process-full.md +123 -18
- package/dist/agent-src/commands/team.md +22 -0
- package/dist/agent-src/contexts/authority/scope-growth-ownership.md +80 -0
- package/dist/agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +47 -0
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +44 -3
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +15 -15
- package/dist/agent-src/contexts/execution/decision-closure-execution.md +93 -0
- package/dist/agent-src/contexts/execution/fix-loop-ladder.md +108 -0
- package/dist/agent-src/contexts/execution/plan-confidence-gate.md +4 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +75 -6
- package/dist/agent-src/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
- package/dist/agent-src/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
- package/dist/agent-src/guidelines/design-fidelity-routing.md +29 -0
- package/dist/agent-src/rules/autonomous-execution.md +15 -8
- package/dist/agent-src/rules/delegation-policy.md +23 -45
- package/dist/agent-src/rules/design-fidelity.md +32 -37
- package/dist/agent-src/rules/evaluator-independence.md +43 -49
- package/dist/agent-src/rules/test-first.md +41 -0
- package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +3 -3
- package/dist/agent-src/skills/ai-council/SKILL.md +11 -3
- package/dist/agent-src/skills/api-design/data/manifest.json +3 -3
- package/dist/agent-src/skills/database/data/manifest.json +4 -4
- package/dist/agent-src/skills/database/data/query-tuning.csv +3 -3
- package/dist/agent-src/skills/design-review/SKILL.md +50 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +14 -4
- package/dist/agent-src/skills/experiment-loop/SKILL.md +31 -1
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/storybook-workshop/SKILL.md +42 -3
- package/dist/agent-src/skills/threat-modeling/SKILL.md +9 -4
- package/dist/agent-src/skills/threat-modeling/data/manifest.json +4 -4
- package/dist/agent-src/skills/verify-repair-loop/SKILL.md +37 -3
- package/dist/agent-src/templates/agent-settings.md +1 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +188 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
- package/dist/cli-delegate/check_update_banner.js +40 -2
- package/dist/cli-delegate/chunk-4E2W63DA.js +335 -0
- package/dist/cli-delegate/chunk-5R7HOLAF.js +99 -0
- package/dist/cli-delegate/{chunk-IVBPRCQO.js → chunk-6KFEC3G7.js} +1 -1
- package/dist/cli-delegate/{chunk-2TAQ2K4V.js → chunk-6KM6ORRY.js} +55 -386
- package/dist/cli-delegate/{chunk-2XY7ILBT.js → chunk-FC75PGJE.js} +42 -2
- package/dist/cli-delegate/{chunk-GYC3VRYN.js → chunk-FXYUYU4N.js} +57 -13
- package/dist/cli-delegate/{chunk-55ACLDUE.js → chunk-GJCBB2UG.js} +168 -10
- package/dist/cli-delegate/{chunk-AK6HODNQ.js → chunk-HDVVGPQW.js} +873 -539
- package/dist/cli-delegate/{chunk-H32DGUJH.js → chunk-P6ZHP5W2.js} +1 -1
- package/dist/cli-delegate/{chunk-57PN4GYJ.js → chunk-RGNIWTEA.js} +1 -1
- package/dist/cli-delegate/chunk-ROYYTEZU.js +39 -0
- package/dist/cli-delegate/{chunk-3FKR64R5.js → chunk-UNCRJBT3.js} +31 -7
- package/dist/cli-delegate/{chunk-2DK2VJWG.js → chunk-WGXTVKCQ.js} +20 -6
- package/dist/cli-delegate/chunk-YDPILCOL.js +152 -0
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +90 -56
- package/dist/cli-delegate/cmd_doctor.js +7 -5
- package/dist/cli-delegate/cmd_explain.js +2 -2
- package/dist/cli-delegate/cmd_export.js +9 -7
- package/dist/cli-delegate/cmd_fleet.js +4 -2
- package/dist/cli-delegate/cmd_handoff.js +4 -4
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +2 -2
- package/dist/cli-delegate/cmd_preflight.js +4 -2
- package/dist/cli-delegate/cmd_prune.js +10 -8
- package/dist/cli-delegate/cmd_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +2 -2
- package/dist/cli-delegate/cmd_settings_get.js +28 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +9 -7
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +10 -8
- package/dist/cli-delegate/cmd_uninstall.js +16 -13
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +12 -10
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +85 -38
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +10 -8
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +8 -6
- package/dist/hooks/dispatch.js +1832 -1007
- package/dist/install/claudeRuleRewrite.js +14 -3
- package/dist/install/conflict.js +60 -10
- package/dist/install/conflictTracker.js +218 -0
- package/dist/install/fsPrimitives.js +54 -0
- package/dist/install/install.mjs +2007 -1481
- package/dist/install/manifestFiles.js +259 -0
- package/dist/install/preflight.js +14 -2
- package/dist/install/preserve.js +157 -0
- package/dist/install/recordedOwnership.js +132 -0
- package/dist/install/txlog.js +11 -2
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +398 -39
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +3 -2
- package/dist/scripts/_lib/fs_atomic.js +167 -0
- package/dist/scripts/_lib/installed_tools.js +460 -0
- package/dist/scripts/_lib/lean_projection_mode.js +3 -2
- package/dist/scripts/_lib/settings_renamed_keys.js +78 -0
- package/dist/server/routes/install.js +8 -0
- package/dist/server/routes/wizard.js +9 -3
- package/dist/server/schemas/settings.js +17 -5
- package/dist/shared/missionExecution.js +97 -0
- package/dist/ui/assets/{index-D0jaCRPM.js → index-DH1xc62v.js} +17 -17
- package/dist/ui/index.html +1 -1
- package/docs/MIGRATION.md +103 -0
- package/docs/contracts/settings-classes.md +26 -7
- package/docs/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
- package/docs/guidelines/design-fidelity-routing.md +29 -0
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
- package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
- package/src/config/agent-settings.template.yml +139 -27
- package/src/config/auto-rule-budget.json +16 -0
- package/src/config/ci-local-parity.yml +18 -5
- package/src/config/continuity-surface.json +21 -0
- package/src/config/gate-coverage.yml +146 -13
- package/src/config/gate-violation-baselines.json +6 -6
- package/src/config/hook-token-budget.json +2 -0
- package/src/config/loop-surfaces.yaml +215 -0
- package/src/config/platform-anchor.json +3 -2
- package/src/config/release-gate-locality.yml +11 -0
- package/src/config/routing-coverage-seed.json +18 -4
- package/src/config/rule-enforcement-baseline.json +0 -1
- package/src/install/claudeRuleRewrite.ts +16 -2
- package/src/install/conflict.ts +65 -9
- package/src/install/conflictTracker.ts +299 -0
- package/src/install/fsPrimitives.ts +54 -0
- package/src/install/manifestFiles.ts +312 -0
- package/src/install/preflight.ts +16 -2
- package/src/install/preserve.ts +205 -0
- package/src/install/recordedOwnership.ts +149 -0
- package/src/install/txlog.ts +11 -2
- package/src/install/types.ts +19 -4
- package/src/scripts/_cli/cmd_conformance.ts +88 -15
- package/src/scripts/_cli/cmd_doctor.ts +4 -3
- package/src/scripts/_cli/cmd_route_explain.ts +9 -2
- package/src/scripts/_cli/cmd_settings_get.ts +44 -0
- package/src/scripts/_cli/doctor_execution.ts +90 -0
- package/src/scripts/_lib/activation_payload.ts +247 -0
- package/src/scripts/_lib/agent_settings.ts +4 -2
- package/src/scripts/_lib/authority_path.ts +102 -0
- package/src/scripts/_lib/cascade_base.ts +94 -0
- package/src/scripts/_lib/changelog_eras.ts +7 -2
- package/src/scripts/_lib/continuation_ladder.ts +149 -2
- package/src/scripts/_lib/council_transport.ts +129 -0
- package/src/scripts/_lib/delivery_ready.ts +112 -0
- package/src/scripts/_lib/forge_protection.ts +267 -0
- package/src/scripts/_lib/hook_platform_keys.ts +27 -0
- package/src/scripts/_lib/json_python_parity.ts +127 -0
- package/src/scripts/_lib/lean_projection_mode.ts +14 -3
- package/src/scripts/_lib/loop_surfaces.ts +382 -0
- package/src/scripts/_lib/mission_record.ts +190 -0
- package/src/scripts/_lib/obligation_frequency.ts +217 -0
- package/src/scripts/_lib/obligations.ts +402 -0
- package/src/scripts/_lib/orchestration_record.ts +11 -20
- package/src/scripts/_lib/release_highlights.ts +199 -10
- package/src/scripts/_lib/release_holds.ts +493 -0
- package/src/scripts/_lib/release_migration_gate.ts +184 -0
- package/src/scripts/_lib/rides_along.ts +102 -0
- package/src/scripts/_lib/settings_renamed_keys.ts +82 -0
- package/src/scripts/_lib/test_delta.ts +166 -0
- package/src/scripts/_lib/test_provenance.ts +171 -0
- package/src/scripts/_lib/typed_op_grant.ts +246 -0
- package/src/scripts/_lib/typed_op_watch.ts +185 -0
- package/src/scripts/ai_council/config.ts +8 -11
- package/src/scripts/ai_council/fallback_config.ts +5 -3
- package/src/scripts/ai_council/ownership_classes.ts +118 -0
- package/src/scripts/ask_block_census.ts +285 -15
- package/src/scripts/check_enforcement_matrix.ts +624 -0
- package/src/scripts/check_gate_reachability.ts +173 -10
- package/src/scripts/check_generator_sync.ts +954 -0
- package/src/scripts/check_preamble_payload_budget.ts +75 -2
- package/src/scripts/check_release_highlights.ts +33 -1
- package/src/scripts/check_release_holds.ts +567 -0
- package/src/scripts/check_rule_projection_integrity.ts +22 -12
- package/src/scripts/check_test_delta.ts +294 -0
- package/src/scripts/check_test_weakening.ts +299 -0
- package/src/scripts/closure_scan.ts +333 -0
- package/src/scripts/compile_hook_manifest.ts +43 -3
- package/src/scripts/council_record_shape.ts +209 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +88 -9
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/design_pass_hook.ts +128 -3
- package/src/scripts/hooks/evidence_independence.ts +96 -0
- package/src/scripts/hooks/memory_recall_hook.ts +540 -0
- package/src/scripts/hooks/obligation_settle_hook.ts +182 -0
- package/src/scripts/hooks/rule_inject_hook.ts +49 -1
- package/src/scripts/hooks/run_continuation_hook.ts +17 -20
- package/src/scripts/hooks_doctor.ts +85 -0
- package/src/scripts/hooks_status.ts +19 -0
- package/src/scripts/install.ts +106 -206
- package/src/scripts/janitor.ts +7 -0
- package/src/scripts/lint_decision_classes.ts +464 -0
- package/src/scripts/lint_hook_manifest.ts +11 -0
- package/src/scripts/lint_major_migration_sections.ts +450 -0
- package/src/scripts/lint_plan_risk_register.ts +47 -3
- package/src/scripts/lint_roadmap_blockers.ts +61 -0
- package/src/scripts/lint_roadmap_producers.ts +202 -0
- package/src/scripts/measure_rule_budget.ts +101 -5
- package/src/scripts/new_roadmap.ts +37 -0
- package/src/scripts/orchestration_record.ts +0 -5
- package/src/scripts/release.ts +14 -98
- package/src/scripts/release_drill.ts +267 -1
- package/src/scripts/release_publication.ts +51 -0
- package/src/scripts/report_enforcement_drift.ts +566 -0
- package/src/scripts/report_host_injection_effect.ts +3 -11
- package/src/scripts/report_standing_payload_by_host.ts +26 -152
- package/src/scripts/schemas/agent-settings.schema.json +23 -0
- package/src/scripts/schemas/command.schema.json +4 -0
- package/src/scripts/scope_growth.ts +219 -0
- package/src/scripts/trigger_eval_grandfather.json +0 -1
- package/src/scripts/ui_conformance_probe.ts +622 -0
- package/src/server/schemas/settings.ts +31 -7
- package/src/shared/missionExecution.ts +126 -0
- package/dist/cli-delegate/chunk-523HCFYO.js +0 -90
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* The closure detector — finds the open decisions in a plan, and classifies
|
|
4
|
+
* each one by OWNERSHIP. It never resolves one.
|
|
5
|
+
*
|
|
6
|
+
* That boundary is the whole design. A decision dispatcher was killed on
|
|
7
|
+
* purpose: no TypeScript path routes a decision, and this script adds none.
|
|
8
|
+
* What it adds is the half that can be mechanical — *which questions are open,
|
|
9
|
+
* and whose they are* — so the claim "this plan reached execution with twelve
|
|
10
|
+
* open technical decisions and asked the owner none of them" is falsifiable
|
|
11
|
+
* rather than a feeling.
|
|
12
|
+
*
|
|
13
|
+
* Detector families, each a phrase an author writes when they KNOW something
|
|
14
|
+
* is open. Deliberately narrow: a detector that fires on ordinary plan prose
|
|
15
|
+
* turns every producer red at once, and the cheapest repair is then to weaken
|
|
16
|
+
* it until it finds nothing.
|
|
17
|
+
*
|
|
18
|
+
* `tbd` TBD / to be decided / decide later
|
|
19
|
+
* `unpicked-alternative` "either X or Y", "option A … option B"
|
|
20
|
+
* `unchecked-assumption` "assuming", "presumably", "probably fine"
|
|
21
|
+
* `missing-verify` an open step with no `verify:` line
|
|
22
|
+
* `ambiguous-acceptance` a vague acceptance criterion (improve / clean up
|
|
23
|
+
* / best practices / handle errors properly)
|
|
24
|
+
* `contradictory` "contradicts", "conflicts with", "but also"
|
|
25
|
+
* `product-semantics` two valid user-visible outcomes
|
|
26
|
+
* `typed-op` an operation from the typed vocabulary
|
|
27
|
+
* `spend` a ceiling or quota question
|
|
28
|
+
*
|
|
29
|
+
* Ownership classification is the output that matters. Six families classify
|
|
30
|
+
* to a technical class the agent, an independent session, a council or a team
|
|
31
|
+
* resolves; exactly two reach the owner (`product-semantics` → `product-owned`,
|
|
32
|
+
* `typed-op` → `destructive-owned`); `spend` reaches nobody — it pauses and
|
|
33
|
+
* reports (ADR-268 § 8).
|
|
34
|
+
*
|
|
35
|
+
* Exit codes: 0 no open decisions · 1 open decisions found · 2 bad argv.
|
|
36
|
+
* Finding open decisions is the POINT, so `--strict` is what makes exit 1 a
|
|
37
|
+
* failure for a caller that wants one; the default is a report.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import * as fs from 'node:fs';
|
|
41
|
+
import * as path from 'node:path';
|
|
42
|
+
import process from 'node:process';
|
|
43
|
+
import { fileURLToPath } from 'node:url';
|
|
44
|
+
|
|
45
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
46
|
+
const _PROG = 'closure_scan';
|
|
47
|
+
|
|
48
|
+
export type Kind =
|
|
49
|
+
| 'tbd'
|
|
50
|
+
| 'unpicked-alternative'
|
|
51
|
+
| 'unchecked-assumption'
|
|
52
|
+
| 'missing-verify'
|
|
53
|
+
| 'ambiguous-acceptance'
|
|
54
|
+
| 'contradictory'
|
|
55
|
+
| 'product-semantics'
|
|
56
|
+
| 'typed-op'
|
|
57
|
+
| 'spend';
|
|
58
|
+
|
|
59
|
+
export interface Finding {
|
|
60
|
+
readonly id: string;
|
|
61
|
+
readonly line: number;
|
|
62
|
+
readonly kind: Kind;
|
|
63
|
+
readonly ownership: string;
|
|
64
|
+
readonly excerpt: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Kind → ownership class. The two owner-routed rows are the whole cost of
|
|
69
|
+
* closure: everything else is resolved without reaching a person, which is
|
|
70
|
+
* what "zero owner questions for twelve technical ambiguities" means.
|
|
71
|
+
*/
|
|
72
|
+
export const OWNERSHIP_BY_KIND: Readonly<Record<Kind, string>> = {
|
|
73
|
+
tbd: 'contested-technical',
|
|
74
|
+
'unpicked-alternative': 'contested-technical',
|
|
75
|
+
'unchecked-assumption': 'reversible-technical',
|
|
76
|
+
'missing-verify': 'deterministic',
|
|
77
|
+
'ambiguous-acceptance': 'reversible-technical',
|
|
78
|
+
contradictory: 'contested-technical',
|
|
79
|
+
'product-semantics': 'product-owned',
|
|
80
|
+
'typed-op': 'destructive-owned',
|
|
81
|
+
spend: 'spend-exhaustion',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** Classes that reach a person. Everything else closes without an ask. */
|
|
85
|
+
export const OWNER_ROUTED: ReadonlySet<string> = new Set([
|
|
86
|
+
'product-owned',
|
|
87
|
+
'business-owned',
|
|
88
|
+
'destructive-owned',
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
interface Pattern {
|
|
92
|
+
readonly kind: Kind;
|
|
93
|
+
readonly re: RegExp;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const LINE_PATTERNS: readonly Pattern[] = [
|
|
97
|
+
{ kind: 'tbd', re: /\bTBD\b/ },
|
|
98
|
+
{ kind: 'tbd', re: /\bto be decided\b/i },
|
|
99
|
+
{ kind: 'tbd', re: /\bdecide later\b/i },
|
|
100
|
+
{ kind: 'unpicked-alternative', re: /\beither\b[^.]{3,80}\bor\b/i },
|
|
101
|
+
{ kind: 'unpicked-alternative', re: /\boption A\b[^.]{0,120}\boption B\b/i },
|
|
102
|
+
{ kind: 'unpicked-alternative', re: /\bwe could (?:either )?\w+[^.]{0,80}\bor\b/i },
|
|
103
|
+
// "two equal technical strategies" is the canonical unpicked alternative,
|
|
104
|
+
// and the phrasing an author reaches for when the choice is genuinely open
|
|
105
|
+
// — it carries no `either`, no `or` and no `option A`.
|
|
106
|
+
{ kind: 'unpicked-alternative', re: /\btwo (?:equal|competing)\b[^.]{0,60}\b(?:strateg|approach|design|option)/i },
|
|
107
|
+
{ kind: 'unchecked-assumption', re: /\bassuming\b/i },
|
|
108
|
+
{ kind: 'unchecked-assumption', re: /\bpresumably\b/i },
|
|
109
|
+
{ kind: 'unchecked-assumption', re: /\bprobably (?:fine|works|ok)\b/i },
|
|
110
|
+
{ kind: 'ambiguous-acceptance', re: /\b(?:improve|optimi[sz]e)\b(?![^.]*\bby\b)/i },
|
|
111
|
+
{ kind: 'ambiguous-acceptance', re: /\bmake it (?:better|cleaner|nicer)\b/i },
|
|
112
|
+
{ kind: 'ambiguous-acceptance', re: /\buse best practices\b/i },
|
|
113
|
+
{ kind: 'ambiguous-acceptance', re: /\bhandle errors properly\b/i },
|
|
114
|
+
{ kind: 'contradictory', re: /\bcontradicts\b/i },
|
|
115
|
+
{ kind: 'contradictory', re: /\bconflicts with\b/i },
|
|
116
|
+
{ kind: 'product-semantics', re: /\buser-visible\b[^.]{0,80}\b(?:or|versus|vs\.?)\b/i },
|
|
117
|
+
{ kind: 'product-semantics', re: /\bwhat the user sees\b/i },
|
|
118
|
+
{ kind: 'product-semantics', re: /\btwo valid (?:user|product) semantics\b/i },
|
|
119
|
+
{ kind: 'typed-op', re: /\b(?:merge|force-push|deploy|publish|purchase|rotate the secret)\b/i },
|
|
120
|
+
{ kind: 'spend', re: /\b(?:over|above|crosses?) the (?:configured )?(?:spend )?ceiling\b/i },
|
|
121
|
+
{ kind: 'spend', re: /\bquota (?:is )?(?:exhausted|gone)\b/i },
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
const STEP_RE = /^\s*-\s\[[ x~-]\]\s/;
|
|
125
|
+
const OPEN_STEP_RE = /^\s*-\s\[ \]\s/;
|
|
126
|
+
const H2_RE = /^##\s+/;
|
|
127
|
+
const DISCHARGE_H2_RE = /^##\s+(Decisions|Blockers|Kill register|Risk Register)\s*$/;
|
|
128
|
+
const ACCEPTANCE_H2_RE = /^##\s+Acceptance Criteria\s*$/;
|
|
129
|
+
|
|
130
|
+
function _isFence(line: string): boolean {
|
|
131
|
+
return /^\s*(```|~~~)/.test(line);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* A scan unit: one logical statement, which is NOT one physical line.
|
|
136
|
+
*
|
|
137
|
+
* A roadmap step wraps over several lines, and the ambiguity it carries
|
|
138
|
+
* routinely straddles the wrap ("either newline-delimited JSON / or a single
|
|
139
|
+
* array"). Matching per line missed exactly those, which are the ones an
|
|
140
|
+
* author is least likely to notice. A step block is therefore ONE unit, joined
|
|
141
|
+
* and matched once — which also means one finding per step at most, so a step
|
|
142
|
+
* carrying two hedges is one open decision rather than two.
|
|
143
|
+
*/
|
|
144
|
+
export interface Unit {
|
|
145
|
+
/** 1-based line of the unit's first line. */
|
|
146
|
+
readonly line: number;
|
|
147
|
+
readonly text: string;
|
|
148
|
+
readonly inAcceptance: boolean;
|
|
149
|
+
readonly isOpenStep: boolean;
|
|
150
|
+
readonly hasVerify: boolean;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function units(lines: readonly string[]): Unit[] {
|
|
154
|
+
const out: Unit[] = [];
|
|
155
|
+
let inFence = false;
|
|
156
|
+
let inDischarge = false;
|
|
157
|
+
let inAcceptance = false;
|
|
158
|
+
let i = 0;
|
|
159
|
+
while (i < lines.length) {
|
|
160
|
+
const line = lines[i] as string;
|
|
161
|
+
if (_isFence(line)) {
|
|
162
|
+
inFence = !inFence;
|
|
163
|
+
i += 1;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (inFence) {
|
|
167
|
+
i += 1;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (H2_RE.test(line)) {
|
|
171
|
+
inDischarge = DISCHARGE_H2_RE.test(line);
|
|
172
|
+
inAcceptance = ACCEPTANCE_H2_RE.test(line);
|
|
173
|
+
}
|
|
174
|
+
if (inDischarge) {
|
|
175
|
+
i += 1;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (STEP_RE.test(line)) {
|
|
179
|
+
let j = i + 1;
|
|
180
|
+
while (j < lines.length && !STEP_RE.test(lines[j] as string) && !H2_RE.test(lines[j] as string)) {
|
|
181
|
+
j += 1;
|
|
182
|
+
}
|
|
183
|
+
const block = lines.slice(i, j);
|
|
184
|
+
out.push({
|
|
185
|
+
line: i + 1,
|
|
186
|
+
// Whitespace-normalised: a wrapped step joined with a single
|
|
187
|
+
// space still carries the next line's indent, so a pattern
|
|
188
|
+
// written with single spaces stops matching across the wrap —
|
|
189
|
+
// which is the case this unit exists to catch.
|
|
190
|
+
text: block.join(' ').replace(/\s+/g, ' '),
|
|
191
|
+
inAcceptance,
|
|
192
|
+
isOpenStep: OPEN_STEP_RE.test(line),
|
|
193
|
+
hasVerify: /(?:^|\s)verify:/m.test(block.join('\n')),
|
|
194
|
+
});
|
|
195
|
+
i = j;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
out.push({
|
|
199
|
+
line: i + 1,
|
|
200
|
+
text: line,
|
|
201
|
+
inAcceptance,
|
|
202
|
+
isOpenStep: false,
|
|
203
|
+
hasVerify: false,
|
|
204
|
+
});
|
|
205
|
+
i += 1;
|
|
206
|
+
}
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function scan(text: string): Finding[] {
|
|
211
|
+
const lines = text.split('\n');
|
|
212
|
+
const findings: Finding[] = [];
|
|
213
|
+
|
|
214
|
+
for (const u of units(lines)) {
|
|
215
|
+
if (/<!--\s*closure:\s*ignore\s*-->/.test(u.text)) continue;
|
|
216
|
+
let matched: Kind | null = null;
|
|
217
|
+
for (const { kind, re } of LINE_PATTERNS) {
|
|
218
|
+
if (re.test(u.text)) {
|
|
219
|
+
matched = kind;
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// An open step carries a `verify:` line — a step whose success cannot
|
|
224
|
+
// be checked is a trigger for a question, not for work. Acceptance
|
|
225
|
+
// criteria are criteria, not steps: they have their own contract and
|
|
226
|
+
// carry no verify line by design.
|
|
227
|
+
if (matched === null && u.isOpenStep && !u.hasVerify && !u.inAcceptance) {
|
|
228
|
+
matched = 'missing-verify';
|
|
229
|
+
}
|
|
230
|
+
if (matched === null) continue;
|
|
231
|
+
findings.push({
|
|
232
|
+
id: '',
|
|
233
|
+
line: u.line,
|
|
234
|
+
kind: matched,
|
|
235
|
+
ownership: OWNERSHIP_BY_KIND[matched],
|
|
236
|
+
excerpt: u.text.trim().replace(/\s+/g, ' ').slice(0, 120),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
findings.sort((a, b) => a.line - b.line);
|
|
241
|
+
return findings.map((f, idx) => ({ ...f, id: `C${String(idx + 1)}` }));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** The `## Decisions` skeleton — one row per finding, resolution left open. */
|
|
245
|
+
export function renderRows(findings: readonly Finding[]): string {
|
|
246
|
+
const header = '| ID | ownership | resolved by | decision | evidence | revisit if |';
|
|
247
|
+
const sep = '|---|---|---|---|---|---|';
|
|
248
|
+
const rows = findings.map(
|
|
249
|
+
(f) => `| ${f.id} | ${f.ownership} | <resolver> | <decision> | ${f.kind}:${String(f.line)} | <condition> |`,
|
|
250
|
+
);
|
|
251
|
+
return [header, sep, ...rows].join('\n');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function ownerQuestionCount(findings: readonly Finding[]): number {
|
|
255
|
+
return findings.filter((f) => OWNER_ROUTED.has(f.ownership)).length;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// --------------------------------------------------------------------------
|
|
259
|
+
// CLI.
|
|
260
|
+
// --------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
function _usage(): string {
|
|
263
|
+
return (
|
|
264
|
+
`usage: ${_PROG} [-h] [--json] [--rows] [--strict] <roadmap.md>\n\n` +
|
|
265
|
+
`Find and classify the open decisions in a plan. Resolves nothing.\n`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function main(argv?: readonly string[]): number {
|
|
270
|
+
const args = argv ?? process.argv.slice(2);
|
|
271
|
+
let json = false;
|
|
272
|
+
let rows = false;
|
|
273
|
+
let strict = false;
|
|
274
|
+
const paths: string[] = [];
|
|
275
|
+
for (const a of args) {
|
|
276
|
+
if (a === '-h' || a === '--help') {
|
|
277
|
+
process.stdout.write(_usage());
|
|
278
|
+
return 0;
|
|
279
|
+
} else if (a === '--json') json = true;
|
|
280
|
+
else if (a === '--rows') rows = true;
|
|
281
|
+
else if (a === '--strict') strict = true;
|
|
282
|
+
else if (a.startsWith('-')) {
|
|
283
|
+
process.stderr.write(`${_usage()}${_PROG}: error: unrecognized arguments: ${a}\n`);
|
|
284
|
+
return 2;
|
|
285
|
+
} else paths.push(a);
|
|
286
|
+
}
|
|
287
|
+
if (paths.length !== 1) {
|
|
288
|
+
process.stderr.write(`${_usage()}${_PROG}: error: exactly one roadmap path is required\n`);
|
|
289
|
+
return 2;
|
|
290
|
+
}
|
|
291
|
+
const target = path.resolve(paths[0] as string);
|
|
292
|
+
let text: string;
|
|
293
|
+
try {
|
|
294
|
+
text = fs.readFileSync(target, 'utf-8');
|
|
295
|
+
} catch {
|
|
296
|
+
process.stderr.write(`${_PROG}: error: cannot read ${paths[0] as string}\n`);
|
|
297
|
+
return 2;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const findings = scan(text);
|
|
301
|
+
const owner = ownerQuestionCount(findings);
|
|
302
|
+
|
|
303
|
+
if (json) {
|
|
304
|
+
process.stdout.write(
|
|
305
|
+
JSON.stringify({ findings, open: findings.length, owner_questions: owner }, null, 2) + '\n',
|
|
306
|
+
);
|
|
307
|
+
} else if (rows) {
|
|
308
|
+
process.stdout.write(renderRows(findings) + '\n');
|
|
309
|
+
} else {
|
|
310
|
+
process.stdout.write(`open decisions: ${String(findings.length)}\n`);
|
|
311
|
+
process.stdout.write(`owner questions: ${String(owner)}\n`);
|
|
312
|
+
for (const f of findings) {
|
|
313
|
+
process.stdout.write(
|
|
314
|
+
` ${f.id} ${String(f.line).padStart(4)} ${f.kind.padEnd(20)} ${f.ownership.padEnd(22)} ${f.excerpt}\n`,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (!findings.length) return 0;
|
|
319
|
+
return strict ? 1 : 0;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function _isCliEntry(): boolean {
|
|
323
|
+
if (!process.argv[1]) return false;
|
|
324
|
+
try {
|
|
325
|
+
return fs.realpathSync(_HERE) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
326
|
+
} catch {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
332
|
+
process.exitCode = main();
|
|
333
|
+
}
|
|
@@ -23,10 +23,21 @@
|
|
|
23
23
|
* Run after editing the manifest:
|
|
24
24
|
* ./scripts-run src/scripts/compile_hook_manifest
|
|
25
25
|
*
|
|
26
|
+
* `--out <path>` writes the compiled bytes THERE and leaves the tree alone. It
|
|
27
|
+
* exists for `check_generator_sync`, whose contract is that a triple regenerates
|
|
28
|
+
* into a temp dir the gate owns and never into the working tree — a gate that
|
|
29
|
+
* repaired the artefact it is measuring would turn every staleness into a silent
|
|
30
|
+
* green plus an unexplained dirty file. Anything that wants to compare against
|
|
31
|
+
* the committed JSON uses this flag; anything that wants to FIX it omits it.
|
|
32
|
+
*
|
|
26
33
|
* `tests/hooks/hook_manifest_compiled.test.ts` fails if the committed JSON
|
|
27
|
-
* stops matching the YAML, so a forgotten run is caught rather than shipped
|
|
34
|
+
* stops matching the YAML, so a forgotten run is caught rather than shipped —
|
|
35
|
+
* but only against the COMMITTED blob in CI. Locally that same test reads the
|
|
36
|
+
* working tree, so a recompile that was run and never committed makes it pass
|
|
37
|
+
* over a defect that is still on the branch. See the discriminator in
|
|
38
|
+
* `check_generator_sync`'s header.
|
|
28
39
|
*
|
|
29
|
-
* Exit codes: 0 written (or already current) · 2 internal error.
|
|
40
|
+
* Exit codes: 0 written (or already current) · 2 internal error or usage.
|
|
30
41
|
*/
|
|
31
42
|
import * as fs from 'node:fs';
|
|
32
43
|
import * as path from 'node:path';
|
|
@@ -46,9 +57,38 @@ export function compile(yamlText: string): string {
|
|
|
46
57
|
});
|
|
47
58
|
}
|
|
48
59
|
|
|
49
|
-
export function main(): number {
|
|
60
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
61
|
+
let outPath: string | null = null;
|
|
62
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
63
|
+
const a = argv[i] as string;
|
|
64
|
+
if (a === '--out') {
|
|
65
|
+
const next = argv[i + 1];
|
|
66
|
+
if (next === undefined || next.startsWith('--')) {
|
|
67
|
+
process.stderr.write('compile_hook_manifest: --out needs a path\n');
|
|
68
|
+
return 2;
|
|
69
|
+
}
|
|
70
|
+
outPath = next;
|
|
71
|
+
i += 1;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
process.stderr.write(
|
|
75
|
+
`compile_hook_manifest: unknown argument ${JSON.stringify(a)}\n` +
|
|
76
|
+
'usage: compile_hook_manifest [--out <path>]\n',
|
|
77
|
+
);
|
|
78
|
+
return 2;
|
|
79
|
+
}
|
|
80
|
+
|
|
50
81
|
const text = fs.readFileSync(YAML_PATH, 'utf-8');
|
|
51
82
|
const next = compile(text);
|
|
83
|
+
|
|
84
|
+
if (outPath !== null) {
|
|
85
|
+
// Comparison copy. No `already current` short-circuit: the caller asked
|
|
86
|
+
// for the bytes, and an unwritten file would read as a generator that
|
|
87
|
+
// silently did nothing.
|
|
88
|
+
fs.writeFileSync(outPath, next);
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
52
92
|
const current = fs.existsSync(JSON_PATH) ? fs.readFileSync(JSON_PATH, 'utf-8') : null;
|
|
53
93
|
if (current === next) {
|
|
54
94
|
process.stdout.write('compile_hook_manifest: already current\n');
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* When a council record owes the owner an options block — and when it must not.
|
|
4
|
+
*
|
|
5
|
+
* The output contract used to end every council run with a numbered-options
|
|
6
|
+
* block carrying the host verdict, unconditionally. Under an ownership axis
|
|
7
|
+
* that is wrong in one specific case and right in every other: a **conclusive
|
|
8
|
+
* technical** verdict is a resolution, and handing it to the owner as options
|
|
9
|
+
* routes a technical decision to a person because it was hard — the move
|
|
10
|
+
* ADR-268 § 10 forbids by name.
|
|
11
|
+
*
|
|
12
|
+
* So the block becomes conditional, on exactly two readable fields:
|
|
13
|
+
*
|
|
14
|
+
* owed the verdict is owner-owned (`product-owned`, `business-owned`,
|
|
15
|
+
* `destructive-owned`), OR the council did not converge. Both
|
|
16
|
+
* are cases where nobody but the owner can close it, and the
|
|
17
|
+
* block carries the council's PROPOSAL for them to confirm.
|
|
18
|
+
* forbidden the verdict is technical AND conclusive. The record states
|
|
19
|
+
* the resolution; the run continues.
|
|
20
|
+
*
|
|
21
|
+
* Convergence is read, not inferred: a record that does not say whether its
|
|
22
|
+
* members converged is itself the finding, because "conclusive" would then be
|
|
23
|
+
* the author's word for it rather than a property of the run.
|
|
24
|
+
*
|
|
25
|
+
* This is an evaluator over a record, not a gate over the tree — the tree's
|
|
26
|
+
* council records are gitignored and pruned (they are dev-time scratch; the
|
|
27
|
+
* durable record is the convergence inlined into the artifact). It exists so
|
|
28
|
+
* the contract is checkable against a fixture rather than asserted in prose.
|
|
29
|
+
*
|
|
30
|
+
* Exit codes: 0 conforming · 1 findings · 2 bad argv.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import * as fs from 'node:fs';
|
|
34
|
+
import * as path from 'node:path';
|
|
35
|
+
import process from 'node:process';
|
|
36
|
+
import { fileURLToPath } from 'node:url';
|
|
37
|
+
|
|
38
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
39
|
+
const _PROG = 'council_record_shape';
|
|
40
|
+
|
|
41
|
+
/** Owner-owned classes — the only ones a council may not close for the owner. */
|
|
42
|
+
export const OWNER_OWNED: ReadonlySet<string> = new Set([
|
|
43
|
+
'product-owned',
|
|
44
|
+
'business-owned',
|
|
45
|
+
'destructive-owned',
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
// Emphasis is stripped before these run (see `_plain`): the label is written
|
|
49
|
+
// `Ownership:`, `**Ownership:**` and `- **Ownership**:` across the corpus, and
|
|
50
|
+
// a pattern that tries to spell all three is how one spelling ends up silently
|
|
51
|
+
// unreadable.
|
|
52
|
+
const OWNERSHIP_RE = /^\s*[-*]?\s*Ownership\s*:\s*`?([a-z-]+)`?/im;
|
|
53
|
+
const CONVERGENCE_RE = /^\s*[-*]?\s*Convergence\s*:\s*(.+)$/im;
|
|
54
|
+
const OPTIONS_HEADING_RE = /^#{2,4}\s+(?:Options|Owner options|User options)\b/im;
|
|
55
|
+
const NUMBERED_OPTION_RE = /^\s*1\.\s+\S/m;
|
|
56
|
+
|
|
57
|
+
export interface RecordShape {
|
|
58
|
+
readonly ownership: string | null;
|
|
59
|
+
readonly convergence: string | null;
|
|
60
|
+
readonly convergent: boolean;
|
|
61
|
+
readonly hasOptionsBlock: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A convergence line is conclusive when it says so in the vocabulary the
|
|
66
|
+
* council already uses: `2/2`, `unanimous`, `convergent`. `split`, `1/2` and
|
|
67
|
+
* `no quorum` are the other side, and they win on conflict — a line reading
|
|
68
|
+
* "2/2 but split" is not a resolution.
|
|
69
|
+
*/
|
|
70
|
+
export function isConvergent(line: string): boolean {
|
|
71
|
+
if (/\b(?:split|divergent|no quorum|abstain|inconclusive)\b/i.test(line)) return false;
|
|
72
|
+
if (/\b(?:unanimous|convergent|conclusive)\b/i.test(line)) return true;
|
|
73
|
+
const m = /(\d+)\s*\/\s*(\d+)/.exec(line);
|
|
74
|
+
if (m) return m[1] === m[2];
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Markdown emphasis removed, line structure kept. */
|
|
79
|
+
export function _plain(text: string): string {
|
|
80
|
+
return text.replace(/\*+/g, '');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function parseRecord(text: string): RecordShape {
|
|
84
|
+
const plain = _plain(text);
|
|
85
|
+
const o = OWNERSHIP_RE.exec(plain);
|
|
86
|
+
const c = CONVERGENCE_RE.exec(plain);
|
|
87
|
+
const convergence = c ? (c[1] as string).trim() : null;
|
|
88
|
+
const headingIdx = OPTIONS_HEADING_RE.exec(plain);
|
|
89
|
+
return {
|
|
90
|
+
ownership: o ? (o[1] as string) : null,
|
|
91
|
+
convergence,
|
|
92
|
+
convergent: convergence === null ? false : isConvergent(convergence),
|
|
93
|
+
hasOptionsBlock: headingIdx !== null && NUMBERED_OPTION_RE.test(plain.slice(headingIdx.index)),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** True when the record must carry an owner-facing options block. */
|
|
98
|
+
export function owesOwnerOptions(shape: RecordShape): boolean {
|
|
99
|
+
if (shape.ownership !== null && OWNER_OWNED.has(shape.ownership)) return true;
|
|
100
|
+
return !shape.convergent;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function checkRecord(rel: string, text: string): string[] {
|
|
104
|
+
const out: string[] = [];
|
|
105
|
+
const shape = parseRecord(text);
|
|
106
|
+
if (shape.ownership === null) {
|
|
107
|
+
out.push(`${rel}: no \`Ownership:\` line — the routing cannot be read`);
|
|
108
|
+
}
|
|
109
|
+
if (shape.convergence === null) {
|
|
110
|
+
out.push(
|
|
111
|
+
`${rel}: no \`Convergence:\` line — "conclusive" would be the author's word for it ` +
|
|
112
|
+
`rather than a property of the run`,
|
|
113
|
+
);
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
const owed = owesOwnerOptions(shape);
|
|
117
|
+
if (owed && !shape.hasOptionsBlock) {
|
|
118
|
+
out.push(
|
|
119
|
+
`${rel}: owner-owned or non-convergent, and carries no owner-facing options block — ` +
|
|
120
|
+
`the owner still has to confirm the council's proposal`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
if (!owed && shape.hasOptionsBlock) {
|
|
124
|
+
out.push(
|
|
125
|
+
`${rel}: a conclusive technical verdict carries an owner-facing options block — ` +
|
|
126
|
+
`that routes a technical decision to the owner because it was hard (ADR-268 § 10)`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function _usage(): string {
|
|
133
|
+
return `usage: ${_PROG} [-h] [--self-test] <record.md>...\n`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function main(argv?: readonly string[]): number {
|
|
137
|
+
const args = argv ?? process.argv.slice(2);
|
|
138
|
+
const paths: string[] = [];
|
|
139
|
+
for (const a of args) {
|
|
140
|
+
if (a === '-h' || a === '--help') {
|
|
141
|
+
process.stdout.write(_usage());
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
if (a === '--self-test') return _selfTest();
|
|
145
|
+
if (a.startsWith('-')) {
|
|
146
|
+
process.stderr.write(`${_usage()}${_PROG}: error: unrecognized arguments: ${a}\n`);
|
|
147
|
+
return 2;
|
|
148
|
+
}
|
|
149
|
+
paths.push(a);
|
|
150
|
+
}
|
|
151
|
+
if (!paths.length) {
|
|
152
|
+
process.stderr.write(`${_usage()}${_PROG}: error: at least one record path is required\n`);
|
|
153
|
+
return 2;
|
|
154
|
+
}
|
|
155
|
+
const findings: string[] = [];
|
|
156
|
+
for (const p of paths) {
|
|
157
|
+
let text: string;
|
|
158
|
+
try {
|
|
159
|
+
text = fs.readFileSync(path.resolve(p), 'utf-8');
|
|
160
|
+
} catch {
|
|
161
|
+
process.stderr.write(`${_PROG}: error: cannot read ${p}\n`);
|
|
162
|
+
return 2;
|
|
163
|
+
}
|
|
164
|
+
findings.push(...checkRecord(p, text));
|
|
165
|
+
}
|
|
166
|
+
if (findings.length) {
|
|
167
|
+
for (const f of findings) process.stderr.write(`❌ ${f}\n`);
|
|
168
|
+
return 1;
|
|
169
|
+
}
|
|
170
|
+
process.stdout.write(`✅ ${_PROG}: ${String(paths.length)} record(s) conform\n`);
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function _selfTest(): number {
|
|
175
|
+
const technical = '- Ownership: contested-technical\n- Convergence: 2/2 convergent\n';
|
|
176
|
+
const product = '- Ownership: product-owned\n- Convergence: 2/2 convergent\n';
|
|
177
|
+
const options = '\n## Options\n\n1. Take the council proposal\n2. Something else\n';
|
|
178
|
+
const failures: string[] = [];
|
|
179
|
+
if (checkRecord('a', technical).length !== 0) failures.push('a conclusive technical verdict red');
|
|
180
|
+
if (checkRecord('b', technical + options).length === 0) {
|
|
181
|
+
failures.push('an options block on a conclusive technical verdict did not red');
|
|
182
|
+
}
|
|
183
|
+
if (checkRecord('c', product + options).length !== 0) failures.push('an owner-owned proposal red');
|
|
184
|
+
if (checkRecord('d', product).length === 0) {
|
|
185
|
+
failures.push('an owner-owned verdict with no proposal did not red');
|
|
186
|
+
}
|
|
187
|
+
if (checkRecord('e', '- Ownership: contested-technical\n- Convergence: 1/2 split\n').length === 0) {
|
|
188
|
+
failures.push('a non-convergent technical verdict with no proposal did not red');
|
|
189
|
+
}
|
|
190
|
+
if (failures.length) {
|
|
191
|
+
for (const f of failures) process.stderr.write(`❌ ${_PROG} --self-test: ${f}\n`);
|
|
192
|
+
return 1;
|
|
193
|
+
}
|
|
194
|
+
process.stdout.write(`✅ ${_PROG} --self-test: all five directions hold\n`);
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function _isCliEntry(): boolean {
|
|
199
|
+
if (!process.argv[1]) return false;
|
|
200
|
+
try {
|
|
201
|
+
return fs.realpathSync(_HERE) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
202
|
+
} catch {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
208
|
+
process.exitCode = main();
|
|
209
|
+
}
|