@zalom/plastic 2.0.0-alpha.9 → 2.0.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/PLASTIC.md +13 -136
- package/README.md +357 -133
- package/agents/plastic-enforcer.md +20 -15
- package/agents/plastic-executor.md +15 -4
- package/agents/plastic-node-research.md +30 -0
- package/agents/plastic-node-verify.md +28 -0
- package/agents/plastic-node-work.md +33 -0
- package/agents/{plastic-advisor.md → plastic-primary-advisor.md} +7 -9
- package/agents/{plastic-faux-advisor.md → plastic-secondary-advisor.md} +9 -12
- package/assets/plastic-logo.svg +1 -0
- package/bin/crap +4 -0
- package/bin/lib/context_budget.rb +43 -6
- package/bin/lib/skill_census.rb +839 -0
- package/bin/plastic +6 -0
- package/bin/plastic-skill-census +114 -0
- package/bin/test +24 -4
- package/bin/verify-change +345 -0
- package/config_asks.yml +4 -4
- package/deprecations.yml +1 -1
- package/{skills/agent-advisor/references → docs/help}/advisor-protocol.md +19 -24
- package/{skills/auto/references → docs/help}/agent-architecture.md +16 -14
- package/{skills/conventions/references → docs/help}/completion-and-done.md +15 -16
- package/docs/help/human-report-contract.md +152 -0
- package/{skills/conventions/references → docs/help}/knowledge-graph.md +9 -0
- package/{skills/conventions/references → docs/help}/locks-and-worktrees.md +11 -13
- package/{skills/conventions/references → docs/help}/maintenance-and-revisions.md +1 -1
- package/{skills/conventions/references → docs/help}/roadmaps.md +2 -2
- package/{skills/tutorial/references → docs/help}/track-1-guided.md +28 -47
- package/{skills/tutorial/references → docs/help}/track-2-auto.md +8 -8
- package/{skills/tutorial/references → docs/help}/track-3-projects-and-roadmaps.md +24 -17
- package/hooks/call-budget +4 -0
- package/hooks/hooks.json +24 -0
- package/hooks/message-display +55 -2
- package/hooks/session-start +5 -1
- package/hooks/statusline +32 -27
- package/hooks/stop +5 -0
- package/package.json +5 -3
- package/scripts/append-ledger +2 -1
- package/scripts/dashboard.rb +267 -16
- package/scripts/day-summary +2 -1
- package/scripts/doctor.rb +680 -39
- package/scripts/end-intent +153 -30
- package/scripts/exec-worktree +5 -5
- package/scripts/file-session-intent +2 -1
- package/scripts/graph-measure +249 -0
- package/scripts/hook-call-budget +222 -0
- package/scripts/hook-capture +24 -123
- package/scripts/hook-close +2 -1
- package/scripts/hook-message-display +7 -0
- package/scripts/hook-record +24 -16
- package/scripts/hook-savepoint +27 -3
- package/scripts/hook-session-start +359 -321
- package/scripts/hook-stop +58 -0
- package/scripts/index-projection +74 -0
- package/scripts/insight-append +17 -4
- package/scripts/install.rb +9 -7
- package/scripts/lib/action_graph_shim.rb +279 -0
- package/scripts/lib/active_delivery.rb +105 -0
- package/scripts/lib/agent_models.rb +63 -25
- package/scripts/lib/arm.rb +48 -83
- package/scripts/lib/atomic_write.rb +31 -0
- package/scripts/lib/backup.rb +65 -0
- package/scripts/lib/cli/command.rb +92 -0
- package/scripts/lib/cli/commands/auto.rb +18 -0
- package/scripts/lib/cli/commands/auto_brief.rb +44 -0
- package/scripts/lib/cli/commands/auto_lock.rb +99 -0
- package/scripts/lib/cli/commands/auto_report.rb +50 -0
- package/scripts/lib/cli/commands/auto_take.rb +31 -0
- package/scripts/lib/cli/commands/backup.rb +43 -0
- package/scripts/lib/cli/commands/checkout.rb +25 -0
- package/scripts/lib/cli/commands/continue.rb +66 -0
- package/scripts/lib/cli/commands/doctor.rb +81 -0
- package/scripts/lib/cli/commands/feedback.rb +40 -0
- package/scripts/lib/cli/commands/help.rb +69 -0
- package/scripts/lib/cli/commands/hook.rb +32 -0
- package/scripts/lib/cli/commands/index.rb +23 -0
- package/scripts/lib/cli/commands/install.rb +21 -0
- package/scripts/lib/cli/commands/installer_verb.rb +37 -0
- package/scripts/lib/cli/commands/intent.rb +19 -0
- package/scripts/lib/cli/commands/intent_answer.rb +37 -0
- package/scripts/lib/cli/commands/intent_command.rb +53 -0
- package/scripts/lib/cli/commands/intent_end.rb +62 -0
- package/scripts/lib/cli/commands/intent_new.rb +62 -0
- package/scripts/lib/cli/commands/intent_note.rb +43 -0
- package/scripts/lib/cli/commands/intent_rule.rb +36 -0
- package/scripts/lib/cli/commands/intent_show.rb +32 -0
- package/scripts/lib/cli/commands/intent_spec.rb +44 -0
- package/scripts/lib/cli/commands/intent_step.rb +71 -0
- package/scripts/lib/cli/commands/intent_verify.rb +26 -0
- package/scripts/lib/cli/commands/migrate.rb +16 -0
- package/scripts/lib/cli/commands/migrate_stores.rb +31 -0
- package/scripts/lib/cli/commands/next.rb +54 -0
- package/scripts/lib/cli/commands/project.rb +19 -0
- package/scripts/lib/cli/commands/project_links.rb +42 -0
- package/scripts/lib/cli/commands/project_list.rb +20 -0
- package/scripts/lib/cli/commands/project_new.rb +72 -0
- package/scripts/lib/cli/commands/query.rb +31 -0
- package/scripts/lib/cli/commands/render.rb +33 -0
- package/scripts/lib/cli/commands/roadmap.rb +19 -0
- package/scripts/lib/cli/commands/roadmap_check.rb +54 -0
- package/scripts/lib/cli/commands/roadmap_log.rb +54 -0
- package/scripts/lib/cli/commands/roadmap_migrate.rb +53 -0
- package/scripts/lib/cli/commands/roadmap_next.rb +70 -0
- package/scripts/lib/cli/commands/roadmap_show.rb +45 -0
- package/scripts/lib/cli/commands/rollback.rb +20 -0
- package/scripts/lib/cli/commands/search.rb +60 -0
- package/scripts/lib/cli/commands/session.rb +18 -0
- package/scripts/lib/cli/commands/session_commit.rb +42 -0
- package/scripts/lib/cli/commands/session_handoff.rb +34 -0
- package/scripts/lib/cli/commands/session_summary.rb +35 -0
- package/scripts/lib/cli/commands/status.rb +68 -0
- package/scripts/lib/cli/commands/subcommand_list.rb +36 -0
- package/scripts/lib/cli/commands/sync.rb +46 -0
- package/scripts/lib/cli/commands/uninstall.rb +20 -0
- package/scripts/lib/cli/commands/update.rb +20 -0
- package/scripts/lib/cli/commands/version.rb +53 -0
- package/scripts/lib/cli/frontier.rb +86 -0
- package/scripts/lib/cli/intent_progress.rb +36 -0
- package/scripts/lib/cli/legacy.rb +80 -0
- package/scripts/lib/cli/output.rb +137 -0
- package/scripts/lib/cli/scope.rb +134 -0
- package/scripts/lib/cli/table.rb +65 -0
- package/scripts/lib/cli.rb +94 -0
- package/scripts/lib/codex_adapter.rb +198 -0
- package/scripts/lib/compact_instructions.rb +13 -5
- package/scripts/lib/core_integrity.rb +71 -0
- package/scripts/lib/dashboard_screen.rb +40 -0
- package/scripts/lib/data_boundary.rb +132 -0
- package/scripts/lib/day_summary.rb +23 -14
- package/scripts/lib/doctor_core.rb +114 -38
- package/scripts/lib/doctor_session_ledger.rb +5 -53
- package/scripts/lib/engine_permissions.rb +88 -0
- package/scripts/lib/exec_worktree.rb +24 -21
- package/scripts/lib/feedback_report.rb +1 -1
- package/scripts/lib/graph_edges.rb +137 -0
- package/scripts/lib/graph_file.rb +246 -0
- package/scripts/lib/graph_measure.rb +645 -0
- package/scripts/lib/graph_measure_budget.rb +409 -0
- package/scripts/lib/graph_measure_cohorts.rb +487 -0
- package/scripts/lib/graph_measure_models.rb +413 -0
- package/scripts/lib/graph_measure_report.rb +532 -0
- package/scripts/lib/graph_tree.rb +98 -0
- package/scripts/lib/guarded_append.rb +155 -0
- package/scripts/lib/handoff.rb +40 -13
- package/scripts/lib/harness_adapter.rb +184 -0
- package/scripts/lib/hook_registry.rb +27 -3
- package/scripts/lib/hook_replay.rb +229 -0
- package/scripts/lib/index_entry.rb +62 -0
- package/scripts/lib/index_projection.rb +201 -0
- package/scripts/lib/insights.rb +1 -1
- package/scripts/lib/installer_core.rb +531 -71
- package/scripts/lib/intent_screen.rb +4 -4
- package/scripts/lib/intent_screen_ansi.rb +73 -12
- package/scripts/lib/intent_validator.rb +2 -2
- package/scripts/lib/lock.rb +10 -11
- package/scripts/lib/message_display.rb +350 -54
- package/scripts/lib/meter_watch.rb +185 -0
- package/scripts/lib/node_file.rb +234 -0
- package/scripts/lib/node_ids.rb +99 -0
- package/scripts/lib/node_input.rb +913 -0
- package/scripts/lib/node_input_compatibility.rb +62 -0
- package/scripts/lib/node_ledger.rb +386 -0
- package/scripts/lib/node_progress.rb +153 -0
- package/scripts/lib/node_return.rb +204 -0
- package/scripts/lib/node_worktree.rb +337 -0
- package/scripts/lib/outcome_report.rb +440 -0
- package/scripts/lib/preflight.rb +4 -6
- package/scripts/lib/project_config.rb +46 -0
- package/scripts/lib/project_validator.rb +3 -2
- package/scripts/lib/qmd_sync.rb +8 -7
- package/scripts/lib/ready_set.rb +462 -0
- package/scripts/lib/reference_archive.rb +45 -0
- package/scripts/lib/release_guard.rb +18 -0
- package/scripts/lib/report_screen.rb +1371 -47
- package/scripts/lib/rlm/corpus.rb +13 -0
- package/scripts/lib/rlm/probe.rb +29 -0
- package/scripts/lib/rlm/query.rb +22 -0
- package/scripts/lib/roadmap_graph.rb +210 -0
- package/scripts/lib/roadmap_migration.rb +95 -0
- package/scripts/lib/roadmap_queue.rb +158 -8
- package/scripts/lib/roadmap_render.rb +150 -0
- package/scripts/lib/roadmap_savepoint.rb +64 -14
- package/scripts/lib/runner_absorb.rb +703 -0
- package/scripts/lib/runner_answer.rb +206 -0
- package/scripts/lib/runner_core.rb +194 -0
- package/scripts/lib/runner_dispatch.rb +525 -0
- package/scripts/lib/runner_policy.rb +191 -0
- package/scripts/lib/runner_proposals.rb +275 -0
- package/scripts/lib/runner_rewind.rb +201 -0
- package/scripts/lib/runner_sweep.rb +231 -0
- package/scripts/lib/runner_until_empty.rb +252 -0
- package/scripts/lib/runner_watch.rb +389 -0
- package/scripts/lib/savepoint.rb +141 -19
- package/scripts/lib/scaffold_intent.rb +6 -3
- package/scripts/lib/screen_paint.rb +365 -28
- package/scripts/lib/screens/dashboard.rb +20 -0
- package/scripts/lib/screens/plan.rb +18 -0
- package/scripts/lib/screens/roadmap.rb +15 -0
- package/scripts/lib/search_index.rb +55 -0
- package/scripts/lib/session_close.rb +30 -28
- package/scripts/lib/session_git.rb +25 -18
- package/scripts/lib/session_ledger.rb +48 -4
- package/scripts/lib/session_usage.rb +190 -0
- package/scripts/lib/sqlite.rb +22 -0
- package/scripts/lib/stop_gate.rb +95 -0
- package/scripts/lib/store_discovery.rb +7 -6
- package/scripts/lib/store_layout.rb +54 -0
- package/scripts/lib/store_provisioning.rb +2 -1
- package/scripts/lib/store_sync.rb +85 -0
- package/scripts/lib/stores_move.rb +93 -0
- package/scripts/lib/verify_intent.rb +36 -8
- package/scripts/lib/version_number.rb +48 -0
- package/scripts/lib/work_graph.rb +59 -0
- package/scripts/lib/work_graph_validator.rb +201 -0
- package/scripts/lib/worktree.rb +27 -32
- package/scripts/lib/worktree_sweep.rb +6 -5
- package/scripts/link-suggest +2 -1
- package/scripts/meter-watch +57 -0
- package/scripts/migrate-to-global +1 -1
- package/scripts/new-intent +4 -13
- package/scripts/node-input +92 -0
- package/scripts/node-run +225 -0
- package/scripts/node-transition +291 -0
- package/scripts/outcome-report +74 -0
- package/scripts/plastic-lock +42 -33
- package/scripts/promote-session-item +3 -2
- package/scripts/read-config +53 -9
- package/scripts/ready-set +126 -0
- package/scripts/release-check +123 -0
- package/scripts/report-screen +177 -16
- package/scripts/roadmap-graph +119 -0
- package/scripts/roadmap-savepoint +7 -0
- package/scripts/runner +581 -0
- package/scripts/savepoint-note +11 -9
- package/scripts/session-commit +2 -1
- package/scripts/session-usage +56 -0
- package/scripts/skill-lint +115 -6
- package/scripts/spawn-preamble +2 -2
- package/scripts/update.rb +25 -4
- package/scripts/validate-work-graph +39 -0
- package/scripts/verify-intent +3 -2
- package/scripts/write-handoff +2 -1
- package/templates/agents.md +7 -7
- package/templates/config.yml +16 -9
- package/templates/dashboard-screen.md +22 -0
- package/templates/display-fixture.md +21 -0
- package/templates/graph.md +16 -0
- package/templates/index.md +1 -1
- package/templates/intent-screen.md +1 -1
- package/templates/node-decision.md +11 -0
- package/templates/node-research.md +13 -0
- package/templates/node-verify.md +13 -0
- package/templates/node-work.md +22 -0
- package/templates/outcome.md +8 -3
- package/templates/project.yml +1 -1
- package/templates/render.css +10 -0
- package/templates/report-plan.md +15 -0
- package/templates/report-roadmap-delivered.md +10 -0
- package/templates/report-roadmap-plan.md +9 -0
- package/templates/report-roadmap-state.md +9 -0
- package/templates/report-state.md +1 -1
- package/templates/roadmap.md +13 -0
- package/bin/plastic.js +0 -70
- package/scripts/lib/bridge.rb +0 -116
- package/skills/agent-advisor/SKILL.md +0 -92
- package/skills/auto/SKILL.md +0 -296
- package/skills/auto/evals/evals.json +0 -255
- package/skills/auto/references/end-tail.md +0 -66
- package/skills/auto/references/human-report-contract.md +0 -78
- package/skills/conventions/SKILL.md +0 -29
- package/skills/dashboard/SKILL.md +0 -169
- package/skills/dashboard/evals/evals.json +0 -38
- package/skills/dashboard/references/classification.md +0 -22
- package/skills/dashboard/templates/dashboard-global.md +0 -20
- package/skills/dashboard/templates/dashboard-project.md +0 -19
- package/skills/direct/SKILL.md +0 -66
- package/skills/direct/references/request-signals.md +0 -59
- package/skills/doctor/SKILL.md +0 -299
- package/skills/doctor/report.md +0 -102
- package/skills/feedback/SKILL.md +0 -98
- package/skills/feedback/references/transport-and-privacy.md +0 -65
- package/skills/feedback/report.md +0 -36
- package/skills/install/SKILL.md +0 -217
- package/skills/intent-continuing/SKILL.md +0 -154
- package/skills/intent-continuing/references/board-fill.md +0 -43
- package/skills/intent-continuing/references/boarding-matrix.md +0 -34
- package/skills/intent-continuing/references/context-management.md +0 -28
- package/skills/intent-continuing/references/liveness-ranking.md +0 -57
- package/skills/intent-creating/SKILL.md +0 -164
- package/skills/intent-creating/evals/evals.json +0 -72
- package/skills/intent-creating/references/lifecycle.md +0 -81
- package/skills/intent-creating/references/wikilinks.md +0 -8
- package/skills/intent-ending/SKILL.md +0 -176
- package/skills/intent-ending/evals/evals.json +0 -74
- package/skills/intent-executing/SKILL.md +0 -170
- package/skills/intent-executing/evals/evals.json +0 -66
- package/skills/intent-executing/implementer-prompt.md +0 -42
- package/skills/intent-executing/spec-reviewer-prompt.md +0 -27
- package/skills/intent-speccing/SKILL.md +0 -130
- package/skills/intent-speccing/evals/evals.json +0 -126
- package/skills/intent-speccing/references/design-principles.md +0 -44
- package/skills/intent-speccing/references/per-section-fill-rules.md +0 -92
- package/skills/intent-speccing/references/self-verify-checklist.md +0 -37
- package/skills/project-creating/SKILL.md +0 -162
- package/skills/project-creating/references/hubs-projects.md +0 -55
- package/skills/project-creating/references/project-scaffolding.md +0 -97
- package/skills/releasing/SKILL.md +0 -337
- package/skills/releasing/references/deprecations.md +0 -60
- package/skills/releasing/references/promotion-and-tagging.md +0 -66
- package/skills/releasing/references/release-lines.md +0 -105
- package/skills/roadmap/SKILL.md +0 -64
- package/skills/roadmap/references/file-format.md +0 -124
- package/skills/roadmap/references/operations.md +0 -112
- package/skills/rollback/SKILL.md +0 -91
- package/skills/tutorial/SKILL.md +0 -65
- package/skills/tutorial/evals/evals.json +0 -186
- package/skills/uninstall/SKILL.md +0 -75
- package/skills/update/SKILL.md +0 -126
- /package/{skills/auto/references → docs/help}/agent-report-contract.md +0 -0
- /package/{skills/intent-executing → docs/help}/code-quality-reviewer-prompt.md +0 -0
- /package/{skills/conventions/references → docs/help}/lifecycle-and-savepoints.md +0 -0
- /package/{skills/intent-executing → docs/help}/plan-reviewer-prompt.md +0 -0
package/scripts/end-intent
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
# 2. Move the intent's INDEX.md line from `## Active` into `## Completed`
|
|
38
38
|
# (delivered) or `## Abandoned` (abandoned), dated today, appending
|
|
39
39
|
# --index-note (if given). Accepts a real em dash OR a plain hyphen as
|
|
40
|
-
# the id/title separator on READ (shared matcher,
|
|
40
|
+
# the id/title separator on READ (shared matcher, IndexEntry.match);
|
|
41
41
|
# always EMITS the real em dash on write. An id that resolves to neither
|
|
42
42
|
# `## Active` nor the terminal section is a loud failure (exit 1); an id
|
|
43
43
|
# already correctly in the terminal section is a quiet, idempotent success.
|
|
@@ -46,10 +46,9 @@
|
|
|
46
46
|
# 5. Disarm (intent 188, D2/D16; intent 307): check the code worktree (derived
|
|
47
47
|
# from projects.yml and the intent id, Arm.worktree_block) for uncommitted
|
|
48
48
|
# changes first (exit 5 if dirty, unless --discard-worktree-changes); call the
|
|
49
|
-
# disarm seam (default Arm.disarm: worktree released, lock cleared
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
# /tmp bridge this step once read was removed in 2.0, intent 307).
|
|
49
|
+
# disarm seam (default Arm.disarm: worktree released, lock cleared); verify
|
|
50
|
+
# the durable lock file is actually gone afterward, and release it directly
|
|
51
|
+
# when a foreign-keyed disarm left it.
|
|
53
52
|
#
|
|
54
53
|
# Exit codes:
|
|
55
54
|
# 0 ok - the intent is closed and no delivery.lock remains.
|
|
@@ -57,7 +56,7 @@
|
|
|
57
56
|
# nor the terminal section (D11). Deliberately double duty; see plan.md.
|
|
58
57
|
# 2 retired in 2.0 (intent 308): the outcome.md guard reports and proceeds.
|
|
59
58
|
# 3 steps 1-4 already committed, but the delivery lock genuinely could not
|
|
60
|
-
# be cleared (
|
|
59
|
+
# be cleared (see step 5 above).
|
|
61
60
|
# 4 pre-flight refusal: a FRESH foreign lock is held by a live,
|
|
62
61
|
# non-delegate session; a FRESH lock that will not parse (corrupt); or a
|
|
63
62
|
# lock file with no resolvable session identity at all. Authors nothing.
|
|
@@ -69,18 +68,20 @@
|
|
|
69
68
|
# 7 hollow delivered report refused (317a D7) - intent 308's one deliberate
|
|
70
69
|
# exception to report-and-proceed; --allow-hollow-report overrides.
|
|
71
70
|
|
|
71
|
+
require_relative "lib/store_layout"
|
|
72
72
|
require "fileutils"
|
|
73
73
|
require "date"
|
|
74
74
|
require "open3"
|
|
75
75
|
require "pathname"
|
|
76
|
-
require_relative "lib/bridge"
|
|
77
76
|
require_relative "lib/arm"
|
|
77
|
+
require_relative "lib/index_entry"
|
|
78
78
|
require_relative "lib/savepoint"
|
|
79
79
|
require_relative "lib/lock"
|
|
80
80
|
require_relative "lib/intent_validator"
|
|
81
81
|
require_relative "lib/outcome_guard"
|
|
82
82
|
require_relative "lib/report_screen"
|
|
83
83
|
require_relative "lib/backfill_intent"
|
|
84
|
+
require_relative "lib/outcome_report"
|
|
84
85
|
|
|
85
86
|
DISPOSITIONS = %w[delivered abandoned].freeze
|
|
86
87
|
|
|
@@ -88,7 +89,7 @@ DISPOSITIONS = %w[delivered abandoned].freeze
|
|
|
88
89
|
# dash (U+2014), the existing INDEX.md convention (store files are exempt from
|
|
89
90
|
# the no-em-dash shipped-file rule). Built from the codepoint, not a literal
|
|
90
91
|
# byte in this source file, so the script's own source stays em-dash free.
|
|
91
|
-
# READING accepts a plain hyphen too, via the shared
|
|
92
|
+
# READING accepts a plain hyphen too, via the shared IndexEntry.match
|
|
92
93
|
# (intent 188, D9/D12): see move_index_to_terminal below.
|
|
93
94
|
EM_DASH = "\u2014"
|
|
94
95
|
|
|
@@ -98,6 +99,10 @@ class UnresolvedIndexEntry < StandardError; end
|
|
|
98
99
|
|
|
99
100
|
# --- Explicit flag parsing (no eval, no global injection) -------------------
|
|
100
101
|
|
|
102
|
+
def blank?(value)
|
|
103
|
+
value.nil? || value.to_s.strip.empty?
|
|
104
|
+
end
|
|
105
|
+
|
|
101
106
|
def parse_args(argv)
|
|
102
107
|
opts = {
|
|
103
108
|
store: nil, id: nil, disposition: nil, index: nil,
|
|
@@ -166,14 +171,65 @@ end
|
|
|
166
171
|
# "projects" (the StoreDiscovery convention, scripts/lib/store_discovery.rb), never by
|
|
167
172
|
# hardcoding ".plastic" as a name.
|
|
168
173
|
def resolve_plastic_home_and_scope(store)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
+
Plastic::StoreLayout.home_and_scope(store)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# --- outcome generation, before the backfill (intent 339, G6, spec D10) -----
|
|
178
|
+
#
|
|
179
|
+
# Generates outcome.md from graph.md, nodes/, and the ledger, BEFORE the
|
|
180
|
+
# backfill step runs, and only into a file that is missing or still the
|
|
181
|
+
# scaffold placeholder (`Savepoint.stage_file_present?` false) - a real,
|
|
182
|
+
# hand-written outcome.md is never touched (row 7.2). Only when a graph.md
|
|
183
|
+
# exists at all (row 7.3): every intent without one closes exactly as it did
|
|
184
|
+
# before this intent. The model is parsed AFTER the `stage_file_present?`
|
|
185
|
+
# guard, not before: a real hand-written outcome.md returns at that guard
|
|
186
|
+
# regardless of graph.md's own health, so a close that was never going to
|
|
187
|
+
# generate anything must never print a parse warning about it. Refuses a
|
|
188
|
+
# report model that reports itself broken (row v1f.2, B2): a malformed
|
|
189
|
+
# `## Graph` fails `model[:ok]` and the close falls straight through to the
|
|
190
|
+
# backfill, rather than adopting parse damage as delivery fact - agreeing
|
|
191
|
+
# with the shipped `outcome-report` CLI, which exits 3 on the same
|
|
192
|
+
# directory. The generated text is then checked against
|
|
193
|
+
# BOTH close gates - `OutcomeGuard` and the hollow-report gate below - before
|
|
194
|
+
# being adopted (row 7.4); when either would refuse it, the write is
|
|
195
|
+
# reverted and the close falls through to intent 308's unchanged backfill
|
|
196
|
+
# (row 7.5), so no close gains an exit 7 it did not have before this intent.
|
|
197
|
+
# Fails open on any exception (row 7.6), the same report-and-proceed shape
|
|
198
|
+
# every other close step in this file uses. `generator:` is the injectable
|
|
199
|
+
# seam (mirrors run_structure_check's `gate:`).
|
|
200
|
+
def run_generate_outcome(intent_dir, disposition:, generator: OutcomeReport.method(:write))
|
|
201
|
+
graph_path = File.join(intent_dir, "graph.md")
|
|
202
|
+
return unless File.exist?(graph_path)
|
|
203
|
+
|
|
204
|
+
outcome_path = File.join(intent_dir, "outcome.md")
|
|
205
|
+
return if Savepoint.stage_file_present?(outcome_path)
|
|
206
|
+
|
|
207
|
+
report_model = OutcomeReport.model(intent_dir)
|
|
208
|
+
unless report_model[:ok]
|
|
209
|
+
warn "end-intent: graph.md failed to parse (#{report_model[:errors].join('; ')}); falling back to the backfill"
|
|
210
|
+
return
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
original_exists = File.exist?(outcome_path)
|
|
214
|
+
original_content = original_exists ? File.read(outcome_path) : nil
|
|
215
|
+
|
|
216
|
+
generator.call(intent_dir, disposition: disposition)
|
|
217
|
+
|
|
218
|
+
guard_reason = OutcomeGuard.reason(intent_dir, disposition)
|
|
219
|
+
hollow = hollow_report_reason(intent_dir, disposition)
|
|
220
|
+
if guard_reason || hollow
|
|
221
|
+
if original_exists
|
|
222
|
+
File.write(outcome_path, original_content)
|
|
223
|
+
elsif File.exist?(outcome_path)
|
|
224
|
+
File.delete(outcome_path)
|
|
225
|
+
end
|
|
226
|
+
warn "end-intent: generated outcome.md would be refused (#{[guard_reason, hollow].compact.join('; ')}); " \
|
|
227
|
+
"falling back to the backfill"
|
|
174
228
|
else
|
|
175
|
-
|
|
229
|
+
warn "end-intent: outcome.md generated from graph.md and the ledger"
|
|
176
230
|
end
|
|
231
|
+
rescue StandardError => e
|
|
232
|
+
warn "end-intent: outcome generation crashed (#{e.message}); proceeding without it"
|
|
177
233
|
end
|
|
178
234
|
|
|
179
235
|
# Backfill (intent 308): write the judgment documents that are missing or still the
|
|
@@ -227,6 +283,48 @@ end
|
|
|
227
283
|
# closed exactly this way and the owner read a screen full of "not recorded".
|
|
228
284
|
# Abandoned closes and machine-backfilled outcomes (the backfill marker) are
|
|
229
285
|
# exempt: the reader cannot expect labels the writer never had.
|
|
286
|
+
#
|
|
287
|
+
# Intent 334 (G1, review A3; post-execution review, blocking 2): the legacy
|
|
288
|
+
# "S3" label grammar applies only to actions/*.md headings, and the node id
|
|
289
|
+
# grammar (kind prefix n/v/d/r plus digits, D1r) applies only to nodes/*.md
|
|
290
|
+
# headings - never both over both directories. This repo's own house words
|
|
291
|
+
# for plan review and post-execution review are "v1" and "v2"; widening the
|
|
292
|
+
# node-id alternative onto actions/ headings turned that prose into a
|
|
293
|
+
# manufactured label and refused legitimate legacy closes at exit 7.
|
|
294
|
+
ACTION_ID_LABEL_RE = /\AS\d+\z/.freeze
|
|
295
|
+
NODE_ID_LABEL_RE = /\A[nvdr][1-9]\d*\z/.freeze
|
|
296
|
+
|
|
297
|
+
# The id-shaped tokens found in `paths`' headings, matched against
|
|
298
|
+
# `id_label_re` only - the caller picks the grammar for the directory.
|
|
299
|
+
def heading_id_labels(paths, id_label_re)
|
|
300
|
+
paths.flat_map do |path|
|
|
301
|
+
ReportScreen.split_by_headings(File.read(path)).flat_map do |heading, _body|
|
|
302
|
+
heading.to_s.sub(/\A#+\s*/, "").split(/[^A-Za-z0-9]+/).select { |tok| tok.match?(id_label_re) }
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# 339 S9 (D18): a node id counts as a label in an actions/*.md heading too,
|
|
308
|
+
# but only when it stands alone as one of the heading's " - "-delimited
|
|
309
|
+
# segments - this repo's own convention, "### S1 - n1 - Title" (this action
|
|
310
|
+
# file's own headings, post-merge) or the bare "### n1 - Title". Never as a
|
|
311
|
+
# free word inside ordinary prose: the comment above ACTION_ID_LABEL_RE
|
|
312
|
+
# records exactly why intent 334 restricted the node-id grammar to
|
|
313
|
+
# nodes/*.md headings in the first place, and that reasoning does not change
|
|
314
|
+
# here - a word-scan widened onto actions/ headings would let this repo's own
|
|
315
|
+
# house words ("v1", "v2" for plan review and post-execution review)
|
|
316
|
+
# manufacture a label out of ordinary prose and refuse a legitimate legacy
|
|
317
|
+
# close. Segment position is what a word-scan cannot see: "v2" embedded
|
|
318
|
+
# mid-sentence is never its own " - "-delimited segment, but a heading built
|
|
319
|
+
# to this convention always puts the node id in one.
|
|
320
|
+
def heading_node_id_segments(paths, id_label_re)
|
|
321
|
+
paths.flat_map do |path|
|
|
322
|
+
ReportScreen.split_by_headings(File.read(path)).flat_map do |heading, _body|
|
|
323
|
+
heading.to_s.sub(/\A#+\s*/, "").split(/\s+-\s+/).map(&:strip).select { |seg| seg.match?(id_label_re) }
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
230
328
|
def hollow_report_reason(intent_dir, disposition)
|
|
231
329
|
return nil unless disposition == "delivered"
|
|
232
330
|
|
|
@@ -235,14 +333,34 @@ def hollow_report_reason(intent_dir, disposition)
|
|
|
235
333
|
text = File.read(outcome)
|
|
236
334
|
return nil if text.include?("<!-- backfilled from the record by end-intent on ")
|
|
237
335
|
|
|
238
|
-
# The gate's evidence bar (A7/B7): it judges only records that CLAIM
|
|
239
|
-
#
|
|
240
|
-
#
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
336
|
+
# The gate's evidence bar (A7/B7): it judges only records that CLAIM a
|
|
337
|
+
# labeled convention - at least one actions/*.md heading carrying an
|
|
338
|
+
# S-label or a node-id segment, or at least one nodes/*.md heading carrying
|
|
339
|
+
# a node id ("n1", "v2", "d3", "r4"). A terse legacy close with no labeled
|
|
340
|
+
# matrix stays report-and-proceed (intent 334, G1, D10r).
|
|
341
|
+
#
|
|
342
|
+
# Every matching token per heading is kept, not just the first (review A3):
|
|
343
|
+
# a legacy actions/ heading can carry BOTH an S-label and prose that reads
|
|
344
|
+
# like a node id ("### The v2 rewrite (S3)"). Restricting the word-scan
|
|
345
|
+
# node-id grammar to nodes/*.md headings (post-execution review, blocking
|
|
346
|
+
# 2) means "v2" in that heading is never even tested against it, so the
|
|
347
|
+
# heading's real "S3" label is never at risk.
|
|
348
|
+
#
|
|
349
|
+
# 339 S9 (D18): a graph-era action file headed by node id alone ("### n1 -
|
|
350
|
+
# Title", no S-label) used to yield an empty label set here and the gate
|
|
351
|
+
# returned before judging anything - every such record closed unjudged by
|
|
352
|
+
# silent exemption. `heading_node_id_segments` widens the actions/*.md side
|
|
353
|
+
# to see that shape without reopening the word-scan hole above: it matches
|
|
354
|
+
# a node id only when the heading's own " - "-delimited segments carry one,
|
|
355
|
+
# never a free word in prose. The node_paths call stays exactly as it was;
|
|
356
|
+
# nodes/*.md headings already resolve node ids correctly (test_node_id_
|
|
357
|
+
# labels_are_recognized) and their heading grammar is controlled, so a
|
|
358
|
+
# word-scan there carries none of the prose risk.
|
|
359
|
+
action_paths = Dir.glob(File.join(intent_dir, "actions", "*.md")).sort
|
|
360
|
+
node_paths = Dir.glob(File.join(intent_dir, "nodes", "*.md")).sort
|
|
361
|
+
heading_labels = (heading_id_labels(action_paths, ACTION_ID_LABEL_RE) +
|
|
362
|
+
heading_node_id_segments(action_paths, NODE_ID_LABEL_RE) +
|
|
363
|
+
heading_id_labels(node_paths, NODE_ID_LABEL_RE)).uniq
|
|
246
364
|
return nil if heading_labels.empty?
|
|
247
365
|
|
|
248
366
|
rows = ReportScreen.delivered_rows(intent_dir)
|
|
@@ -317,10 +435,10 @@ def section_stop(lines, heading_idx)
|
|
|
317
435
|
stop
|
|
318
436
|
end
|
|
319
437
|
|
|
320
|
-
# Shared matcher (
|
|
438
|
+
# Shared matcher (IndexEntry.match, intent 188 D12): accepts a real em
|
|
321
439
|
# dash OR a plain hyphen as the id/title separator on READ.
|
|
322
440
|
def active_line_id(line)
|
|
323
|
-
m =
|
|
441
|
+
m = IndexEntry.match(line)
|
|
324
442
|
m && m[1]
|
|
325
443
|
end
|
|
326
444
|
|
|
@@ -399,7 +517,7 @@ def move_index_to_terminal(content, id, disposition, today:, index_note: nil)
|
|
|
399
517
|
raise UnresolvedIndexEntry, "INDEX.md has no #{target_heading} heading; cannot complete the move"
|
|
400
518
|
end
|
|
401
519
|
|
|
402
|
-
m =
|
|
520
|
+
m = IndexEntry.match(lines[entry_idx])
|
|
403
521
|
title, link = m[2], m[3]
|
|
404
522
|
note_suffix = (index_note && !index_note.to_s.strip.empty?) ? " #{index_note.to_s.strip}" : ""
|
|
405
523
|
new_entry = "- [#{id} #{EM_DASH} #{title}](#{link}) #{EM_DASH} #{today}#{note_suffix}\n"
|
|
@@ -488,13 +606,13 @@ end
|
|
|
488
606
|
# exclusion with a raw backtrace. nil is the only safe "could not resolve"
|
|
489
607
|
# signal; main refuses explicitly on it rather than ever reaching takeover.
|
|
490
608
|
def resolve_end_session(explicit, intent_dir)
|
|
491
|
-
return explicit.to_s.strip unless
|
|
609
|
+
return explicit.to_s.strip unless blank?(explicit)
|
|
492
610
|
env = ENV["CLAUDE_CODE_SESSION_ID"]
|
|
493
|
-
return env.to_s.strip unless
|
|
611
|
+
return env.to_s.strip unless blank?(env)
|
|
494
612
|
lock = Lock.read(intent_dir)
|
|
495
613
|
return nil unless lock
|
|
496
614
|
owner = lock["owner_session"].to_s
|
|
497
|
-
|
|
615
|
+
blank?(owner) ? nil : owner
|
|
498
616
|
end
|
|
499
617
|
|
|
500
618
|
# Read-only pre-flight verdict (D4). Returns [:proceed, lock_or_nil],
|
|
@@ -542,7 +660,7 @@ end
|
|
|
542
660
|
def run_disarm(intent_dir, id, session, discard_worktree_changes:,
|
|
543
661
|
worktree_reader: ->(dir) { Arm.worktree_block(intent_dir: dir) },
|
|
544
662
|
disarm: ->(sess, dir) { Arm.disarm(intent_dir: dir, session: sess) })
|
|
545
|
-
return :ok if
|
|
663
|
+
return :ok if blank?(session)
|
|
546
664
|
|
|
547
665
|
block = worktree_reader.call(intent_dir)
|
|
548
666
|
worktree_code = block.is_a?(Hash) ? block["code"] : nil
|
|
@@ -667,7 +785,7 @@ def main(argv)
|
|
|
667
785
|
# Lock.takeover (blank session reaches Lock.acquire's blank? guard AFTER the
|
|
668
786
|
# existing lock file was already deleted) or silently mis-arbitrate. Refuse
|
|
669
787
|
# rather than guess; nothing has been written yet.
|
|
670
|
-
if
|
|
788
|
+
if blank?(session) && File.exist?(Lock.path(intent_dir))
|
|
671
789
|
warn "end-intent: a delivery lock exists at #{Lock.path(intent_dir)} but no session " \
|
|
672
790
|
"identity could be resolved (--session, CLAUDE_CODE_SESSION_ID, and the lock's " \
|
|
673
791
|
"own recorded owner are all blank); refusing rather than guessing. Pass " \
|
|
@@ -718,6 +836,11 @@ def main(argv)
|
|
|
718
836
|
end
|
|
719
837
|
end
|
|
720
838
|
|
|
839
|
+
# 0a. Generate outcome.md from graph.md and the ledger, when there is one
|
|
840
|
+
# (intent 339, G6, spec D10) - before the backfill, so a real graph never
|
|
841
|
+
# falls through to the terse backfill it exists to replace.
|
|
842
|
+
run_generate_outcome(intent_dir, disposition: disposition)
|
|
843
|
+
|
|
721
844
|
# 1. Backfill from the record (intent 308), then the structure self-check and the
|
|
722
845
|
# outcome guard, both report-only since 308.
|
|
723
846
|
run_backfill(intent_dir, store: store, id: id, disposition: disposition,
|
package/scripts/exec-worktree
CHANGED
|
@@ -21,15 +21,15 @@
|
|
|
21
21
|
# (--session, else CLAUDE_CODE_SESSION_ID, else the existing delivery.lock's own
|
|
22
22
|
# recorded owner_session when non-blank, else nil; the lock-owner fallback IS
|
|
23
23
|
# used here, matching end-intent's teardown resolution order).
|
|
24
|
-
# Derive the worktree block from projects.yml and the intent id (Arm.worktree_block
|
|
25
|
-
#
|
|
24
|
+
# Derive the worktree block from projects.yml and the intent id (Arm.worktree_block).
|
|
25
|
+
# No code worktree on
|
|
26
26
|
# disk means nothing was provisioned (exit 0).
|
|
27
27
|
# 1. The dirty-worktree guard (delivered only): git status --porcelain on the code
|
|
28
28
|
# worktree. Not clean, or the check itself fails, fails CLOSED (exit 3), mirroring
|
|
29
29
|
# end-intent's dirty-worktree handling. abandoned skips this guard entirely: the
|
|
30
30
|
# branch survives and can be reclaimed, so a dirty worktree is not a reason to
|
|
31
31
|
# refuse.
|
|
32
|
-
# 2. Finish: Worktree.finish(
|
|
32
|
+
# 2. Finish: Worktree.finish(delivery, home:, runner:, merge: disposition ==
|
|
33
33
|
# "delivered"). Worktree.finish is fail-open and never raises, so failure is
|
|
34
34
|
# detected by checking, for a delivered disposition only, whether the worktree
|
|
35
35
|
# directory still exists on disk afterward (release removes it on success).
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
# one concern per script (spec D8).
|
|
39
39
|
#
|
|
40
40
|
# Exit codes:
|
|
41
|
-
# 0 finished (merged or removed per disposition), or nothing to finish (no
|
|
42
|
-
# resolved, or no code worktree recorded on
|
|
41
|
+
# 0 finished (merged or removed per disposition), or nothing to finish (no delivery
|
|
42
|
+
# resolved, or no code worktree recorded on it)
|
|
43
43
|
# 1 usage or path-resolution failure
|
|
44
44
|
# 2 retired in 2.0 (intent 302): was the order precondition; never emitted, never reused
|
|
45
45
|
# 3 the worktree is not clean, or the merge/removal did not complete (delivered only)
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
# and on any filing error (reported on stderr; the next boot tries again), 2 on
|
|
15
15
|
# a usage error.
|
|
16
16
|
|
|
17
|
+
require_relative "lib/store_layout"
|
|
17
18
|
require_relative "lib/session_ledger"
|
|
18
19
|
require_relative "lib/session_backfill"
|
|
19
20
|
|
|
@@ -48,7 +49,7 @@ if opts[:carry_to] && !SessionLedger.valid_day_id?(opts[:carry_to])
|
|
|
48
49
|
usage_abort("--carry-to must be a real calendar date, YYYYMMDD")
|
|
49
50
|
end
|
|
50
51
|
|
|
51
|
-
store = opts[:store] ||
|
|
52
|
+
store = opts[:store] || Plastic::StoreLayout.global_store(File.expand_path(ENV.fetch("PLASTIC_HOME", "~/.plastic")))
|
|
52
53
|
templates = opts[:templates] || File.expand_path("../templates", __dir__)
|
|
53
54
|
|
|
54
55
|
begin
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
require "json"
|
|
6
|
+
require "yaml"
|
|
7
|
+
require "date"
|
|
8
|
+
require_relative "lib/savepoint"
|
|
9
|
+
|
|
10
|
+
# graph-measure - the one read-only executable over the node ledger (intent
|
|
11
|
+
# 343, G10, n2). A subcommand table over one library, following
|
|
12
|
+
# scripts/runner's shape (327 spec): the public verbs are named up front even
|
|
13
|
+
# before every module lands, a flag outside a verb's own vocabulary is
|
|
14
|
+
# refused by name, and each verb's module is required LAZILY inside its own
|
|
15
|
+
# branch so a verb whose module has not landed yet reports plainly instead of
|
|
16
|
+
# taking `intent`, the verb an operator runs constantly, down with it.
|
|
17
|
+
#
|
|
18
|
+
# Usage:
|
|
19
|
+
# graph-measure <intent|budget|cohorts> <dir> [--format text|json]
|
|
20
|
+
#
|
|
21
|
+
# `intent` (n2) and `budget` (n4) take an INTENT directory. `cohorts` (n5,
|
|
22
|
+
# its model section; n6 adds the rates, the latency and the bar to the same
|
|
23
|
+
# verb) takes a STORE directory instead - the directory holding `id--slug/`
|
|
24
|
+
# intent folders for one project - and is validated against that shape, not
|
|
25
|
+
# `intent_directory?`.
|
|
26
|
+
#
|
|
27
|
+
# Exit codes:
|
|
28
|
+
# 0 - a report rendered (anomalies never change this: a ledger with torn
|
|
29
|
+
# lines still renders, lists them, and exits 0)
|
|
30
|
+
# 1 - the directory has no savepoint.md to measure at all, named
|
|
31
|
+
# 2 - usage: no args, an unknown subcommand, an unknown flag, or the path
|
|
32
|
+
# is not the right shape for its verb (an intent directory for
|
|
33
|
+
# `intent`/`budget`, a store directory for `cohorts`)
|
|
34
|
+
# 3 - the verb's module has not been delivered yet
|
|
35
|
+
module GraphMeasureCLI
|
|
36
|
+
module_function
|
|
37
|
+
|
|
38
|
+
VERBS = %w[intent budget cohorts].freeze
|
|
39
|
+
SAVEPOINT_FILE = "savepoint.md"
|
|
40
|
+
|
|
41
|
+
# verb -> [[ModuleName, lib_file], ...], required lazily inside the verb's
|
|
42
|
+
# own branch (row 2.6). "budget" and "cohorts" name libraries n4 and n5
|
|
43
|
+
# ship; requiring them today raises LoadError, caught below.
|
|
44
|
+
LAZY_MODULES = {
|
|
45
|
+
"intent" => [["GraphMeasure", "graph_measure"], ["GraphMeasureReport", "graph_measure_report"]],
|
|
46
|
+
"budget" => [["GraphMeasureBudget", "graph_measure_budget"]],
|
|
47
|
+
"cohorts" => [["GraphMeasureModels", "graph_measure_models"], ["GraphMeasureCohorts", "graph_measure_cohorts"]],
|
|
48
|
+
}.freeze
|
|
49
|
+
|
|
50
|
+
KNOWN_FLAGS = {
|
|
51
|
+
"intent" => %w[--format],
|
|
52
|
+
"budget" => %w[--format],
|
|
53
|
+
"cohorts" => %w[--format],
|
|
54
|
+
}.freeze
|
|
55
|
+
|
|
56
|
+
# "intent" and "budget" take one intent directory; "cohorts" takes the
|
|
57
|
+
# store directory that holds many of them (row 5.15).
|
|
58
|
+
DIR_KIND = { "intent" => :intent, "budget" => :intent, "cohorts" => :store }.freeze
|
|
59
|
+
|
|
60
|
+
def usage
|
|
61
|
+
warn "Usage: graph-measure <#{VERBS.join('|')}> <dir> [--format text|json]"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def intent_directory?(dir)
|
|
65
|
+
dir && File.directory?(dir) && File.exist?(Savepoint.intent_file(dir))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def store_directory?(dir)
|
|
69
|
+
dir && File.directory?(dir)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def valid_dir?(verb, dir)
|
|
73
|
+
DIR_KIND.fetch(verb, :intent) == :store ? store_directory?(dir) : intent_directory?(dir)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def opt(args, name)
|
|
77
|
+
(i = args.index(name)) && args[i + 1]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def unrecognized_flag(verb, args)
|
|
81
|
+
allowed = KNOWN_FLAGS.fetch(verb, [])
|
|
82
|
+
args.find { |a| a.is_a?(String) && a.start_with?("--") && !allowed.include?(a) }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def main(argv)
|
|
86
|
+
args = argv.dup
|
|
87
|
+
verb = args.shift
|
|
88
|
+
|
|
89
|
+
unless verb
|
|
90
|
+
usage
|
|
91
|
+
return 2
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
unless VERBS.include?(verb)
|
|
95
|
+
warn "graph-measure: unknown subcommand #{verb.inspect}"
|
|
96
|
+
usage
|
|
97
|
+
return 2
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
dir_arg = args.shift
|
|
101
|
+
unless valid_dir?(verb, dir_arg && File.expand_path(dir_arg))
|
|
102
|
+
label = DIR_KIND.fetch(verb, :intent) == :store ? "not a store directory" : "not an intent directory"
|
|
103
|
+
warn "graph-measure: #{dir_arg.inspect} is #{label}"
|
|
104
|
+
usage
|
|
105
|
+
return 2
|
|
106
|
+
end
|
|
107
|
+
dir = File.expand_path(dir_arg)
|
|
108
|
+
|
|
109
|
+
bad_flag = unrecognized_flag(verb, args)
|
|
110
|
+
if bad_flag
|
|
111
|
+
warn "graph-measure: unknown flag #{bad_flag.inspect} for #{verb}"
|
|
112
|
+
usage
|
|
113
|
+
return 2
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
format = opt(args, "--format") || "text"
|
|
117
|
+
unless %w[text json].include?(format)
|
|
118
|
+
warn "graph-measure: unknown --format #{format.inspect} (use text or json)"
|
|
119
|
+
usage
|
|
120
|
+
return 2
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
run_lazy(verb, dir, format)
|
|
124
|
+
rescue StandardError => e
|
|
125
|
+
# Row 2.22: a malformed graph.md, a torn ledger, or invalid UTF-8 never
|
|
126
|
+
# raises out of GraphMeasure or GraphMeasureReport (both are built to
|
|
127
|
+
# resolve broken data to an anomaly, never an exception) - this is
|
|
128
|
+
# defence in depth so a defect in either never reaches the owner as a
|
|
129
|
+
# bare Ruby backtrace.
|
|
130
|
+
warn "graph-measure: internal error: #{e.message}"
|
|
131
|
+
1
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def run_lazy(verb, dir, format)
|
|
135
|
+
LAZY_MODULES.fetch(verb, []).each { |(_name, file)| require_relative(File.join("lib", file)) }
|
|
136
|
+
rescue LoadError
|
|
137
|
+
warn "graph-measure: #{verb} is not yet delivered (its module has not landed)"
|
|
138
|
+
3
|
|
139
|
+
else
|
|
140
|
+
dispatch_lazy(verb, dir, format)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def dispatch_lazy(verb, dir, format)
|
|
144
|
+
case verb
|
|
145
|
+
when "intent"
|
|
146
|
+
run_intent(dir, format)
|
|
147
|
+
when "budget"
|
|
148
|
+
run_budget(dir, format)
|
|
149
|
+
when "cohorts"
|
|
150
|
+
run_cohorts(dir, format)
|
|
151
|
+
else
|
|
152
|
+
warn "graph-measure: #{verb}'s module loaded but no dispatcher is wired up yet"
|
|
153
|
+
3
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# M1: the boundary's own config resolution, never GraphMeasureModels' or
|
|
158
|
+
# GraphMeasureCohorts' (spec D2, both modules' own DI convention). Mirrors
|
|
159
|
+
# scripts/doctor.rb's own `PLASTIC_HOME` fallback and `load_yaml_safe`
|
|
160
|
+
# shape exactly, so a missing or malformed config.yml/project.yml resolves
|
|
161
|
+
# to `{}` (the shipped-default chain), never an exception.
|
|
162
|
+
def plastic_home
|
|
163
|
+
ENV.fetch("PLASTIC_HOME") { File.join(Dir.home, ".plastic") }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def load_yaml_safe(path)
|
|
167
|
+
return {} unless path && File.exist?(path)
|
|
168
|
+
|
|
169
|
+
YAML.safe_load(File.read(path), permitted_classes: [Date, Time]) || {}
|
|
170
|
+
rescue StandardError
|
|
171
|
+
{}
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def missing_savepoint(dir)
|
|
175
|
+
savepoint_path = File.join(dir, SAVEPOINT_FILE)
|
|
176
|
+
return nil if File.exist?(savepoint_path)
|
|
177
|
+
|
|
178
|
+
warn "graph-measure: #{savepoint_path} does not exist, nothing to measure"
|
|
179
|
+
savepoint_path
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def run_intent(dir, format)
|
|
183
|
+
# Row 2.5: checked directly, ahead of GraphMeasure.read, which resolves a
|
|
184
|
+
# missing file to an empty (not absent) record by design (D3) - the CLI
|
|
185
|
+
# boundary is the only place that can still tell "nothing to measure"
|
|
186
|
+
# apart from "measured, and it is empty".
|
|
187
|
+
return 1 if missing_savepoint(dir)
|
|
188
|
+
|
|
189
|
+
record = GraphMeasure.read(dir)
|
|
190
|
+
puts format == "json" ? GraphMeasureReport.render_json(record) : GraphMeasureReport.render_text(record)
|
|
191
|
+
0
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def run_budget(dir, format)
|
|
195
|
+
return 1 if missing_savepoint(dir)
|
|
196
|
+
|
|
197
|
+
record = GraphMeasureBudget.read(dir)
|
|
198
|
+
puts format == "json" ? GraphMeasureBudget.render_json(record) : GraphMeasureBudget.render_text(record)
|
|
199
|
+
0
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Row 2.5's "missing savepoint.md" check does not apply here: `dir` is a
|
|
203
|
+
# STORE directory (spec D1's "the directory holding id--slug/ intent
|
|
204
|
+
# folders"), which never carries a savepoint.md of its own.
|
|
205
|
+
#
|
|
206
|
+
# n6 adds the rates, the latency and the bar to this same verb, sourced
|
|
207
|
+
# from `GraphMeasureCohorts`. Its bar section reuses the one
|
|
208
|
+
# `GraphMeasureModels` walk's `qualified` counts (passed in here) rather
|
|
209
|
+
# than triggering a second store walk for the same numbers.
|
|
210
|
+
#
|
|
211
|
+
# M1 (v1 review): this call used to pass neither `project_config:` nor
|
|
212
|
+
# `global_config:`, so the model comparison always resolved against
|
|
213
|
+
# RunnerPolicy's shipped defaults only, never the real project-then-
|
|
214
|
+
# global-then-default chain the CHANGELOG describes and
|
|
215
|
+
# `scripts/doctor.rb`'s own `model_requalification_checks` actually reads.
|
|
216
|
+
# Reproduced: with a `plastic-executor:` override present in global
|
|
217
|
+
# config, `GraphMeasureModels.read` reports real drift rows while this
|
|
218
|
+
# verb (unpatched) always reported zero. `GraphMeasure`'s own modules stay
|
|
219
|
+
# config-free by design (D2's DI convention: they never read disk or ENV
|
|
220
|
+
# themselves) - this executable is the boundary that is allowed to, the
|
|
221
|
+
# same role `scripts/doctor.rb` already plays for the same two config
|
|
222
|
+
# files.
|
|
223
|
+
#
|
|
224
|
+
# NEW-4 (v2 review, D22): the project-scope half named `<store>/../project.yml`,
|
|
225
|
+
# a file whose template carries no `agents:` key at all, so a real
|
|
226
|
+
# operator override at the file that DOES exist in the wild - the store's
|
|
227
|
+
# sibling `config.yml` - was silently ignored. The path now comes from
|
|
228
|
+
# `GraphMeasureModels.project_config_path`, the one function
|
|
229
|
+
# `scripts/doctor.rb`'s own rule calls too, so the two commands cannot
|
|
230
|
+
# resolve a different file for the same store.
|
|
231
|
+
def run_cohorts(dir, format)
|
|
232
|
+
global_config = load_yaml_safe(File.join(plastic_home, "config.yml"))
|
|
233
|
+
project_config = load_yaml_safe(GraphMeasureModels.project_config_path(dir))
|
|
234
|
+
|
|
235
|
+
models_record = GraphMeasureModels.read(dir, project_config: project_config, global_config: global_config)
|
|
236
|
+
cohorts_record = GraphMeasureCohorts.read(dir, qualified: models_record[:qualified],
|
|
237
|
+
project_config: project_config, global_config: global_config)
|
|
238
|
+
if format == "json"
|
|
239
|
+
combined = GraphMeasureModels.model(models_record).merge(GraphMeasureCohorts.model(cohorts_record))
|
|
240
|
+
puts JSON.generate(combined)
|
|
241
|
+
else
|
|
242
|
+
puts GraphMeasureModels.render_text(models_record)
|
|
243
|
+
puts GraphMeasureCohorts.render_text(cohorts_record)
|
|
244
|
+
end
|
|
245
|
+
0
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
exit GraphMeasureCLI.main(ARGV) if $PROGRAM_NAME == __FILE__
|