@meetless/mla 0.1.4

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.
Files changed (202) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +81 -0
  3. package/dist/build-info.json +9 -0
  4. package/dist/bundles/ask-core.js +396 -0
  5. package/dist/bundles/mcp.js +16592 -0
  6. package/dist/bundles/trace-core.js +263 -0
  7. package/dist/cli.js +828 -0
  8. package/dist/commands/activate.js +781 -0
  9. package/dist/commands/adoption.js +130 -0
  10. package/dist/commands/ask.js +290 -0
  11. package/dist/commands/context.js +114 -0
  12. package/dist/commands/debug.js +313 -0
  13. package/dist/commands/doctor.js +1021 -0
  14. package/dist/commands/enrich.js +427 -0
  15. package/dist/commands/evidence.js +229 -0
  16. package/dist/commands/flush.js +184 -0
  17. package/dist/commands/graph.js +104 -0
  18. package/dist/commands/init.js +272 -0
  19. package/dist/commands/internal-active-review.js +322 -0
  20. package/dist/commands/internal-auto-index.js +188 -0
  21. package/dist/commands/internal-capture-decisions.js +320 -0
  22. package/dist/commands/internal-evidence-correlate.js +239 -0
  23. package/dist/commands/internal-evidence-hooks.js +240 -0
  24. package/dist/commands/internal-evidence-inject.js +231 -0
  25. package/dist/commands/internal-finalize.js +221 -0
  26. package/dist/commands/internal-pretool-observe.js +225 -0
  27. package/dist/commands/internal-refresh.js +136 -0
  28. package/dist/commands/internal-session-nudge.js +120 -0
  29. package/dist/commands/internal-steer-sync.js +117 -0
  30. package/dist/commands/internal-turn-recap.js +140 -0
  31. package/dist/commands/kb.js +375 -0
  32. package/dist/commands/kb_add.js +681 -0
  33. package/dist/commands/kb_forget.js +283 -0
  34. package/dist/commands/kb_move.js +45 -0
  35. package/dist/commands/kb_pending.js +410 -0
  36. package/dist/commands/kb_personal.js +149 -0
  37. package/dist/commands/kb_promote.js +188 -0
  38. package/dist/commands/kb_purge.js +168 -0
  39. package/dist/commands/kb_reingest.js +335 -0
  40. package/dist/commands/kb_retime.js +170 -0
  41. package/dist/commands/kb_review.js +391 -0
  42. package/dist/commands/kb_revision.js +179 -0
  43. package/dist/commands/kb_show.js +385 -0
  44. package/dist/commands/label.js +226 -0
  45. package/dist/commands/login.js +295 -0
  46. package/dist/commands/logout.js +108 -0
  47. package/dist/commands/mcp-supervisor.js +93 -0
  48. package/dist/commands/mcp.js +227 -0
  49. package/dist/commands/queue-prune.js +98 -0
  50. package/dist/commands/review.js +358 -0
  51. package/dist/commands/rewire.js +124 -0
  52. package/dist/commands/rules.js +728 -0
  53. package/dist/commands/scan-context.js +67 -0
  54. package/dist/commands/session.js +347 -0
  55. package/dist/commands/stats.js +479 -0
  56. package/dist/commands/status.js +61 -0
  57. package/dist/commands/summary.js +250 -0
  58. package/dist/commands/turn.js +114 -0
  59. package/dist/commands/uninstall.js +222 -0
  60. package/dist/commands/whoami.js +102 -0
  61. package/dist/commands/workspace.js +130 -0
  62. package/dist/hooks-template/ce0-post-tool-use.sh +34 -0
  63. package/dist/hooks-template/ce0-session-start.sh +49 -0
  64. package/dist/hooks-template/ce0-stop.sh +29 -0
  65. package/dist/hooks-template/ce0-user-prompt-submit.sh +38 -0
  66. package/dist/hooks-template/common.sh +934 -0
  67. package/dist/hooks-template/event-batch-filter.jq +67 -0
  68. package/dist/hooks-template/flush.sh +503 -0
  69. package/dist/hooks-template/post-tool-use.sh +423 -0
  70. package/dist/hooks-template/pre-tool-use.sh +69 -0
  71. package/dist/hooks-template/session-start.sh +140 -0
  72. package/dist/hooks-template/stop.sh +308 -0
  73. package/dist/hooks-template/user-prompt-submit.sh +1162 -0
  74. package/dist/lib/activation.js +79 -0
  75. package/dist/lib/active-conflict-cache.js +141 -0
  76. package/dist/lib/active-memory.js +59 -0
  77. package/dist/lib/active-review-runner.js +26 -0
  78. package/dist/lib/agent-decision/index.js +25 -0
  79. package/dist/lib/agent-decision/keys.js +49 -0
  80. package/dist/lib/agent-decision/normalize-claude.js +183 -0
  81. package/dist/lib/agent-decision/types.js +21 -0
  82. package/dist/lib/agent-decision/validate.js +216 -0
  83. package/dist/lib/analytics/capture.js +96 -0
  84. package/dist/lib/analytics/command-event.js +267 -0
  85. package/dist/lib/analytics/consent.js +58 -0
  86. package/dist/lib/analytics/coverage-gap.js +96 -0
  87. package/dist/lib/analytics/envelope.js +236 -0
  88. package/dist/lib/analytics/event-id.js +86 -0
  89. package/dist/lib/analytics/evidence.js +150 -0
  90. package/dist/lib/analytics/followthrough.js +194 -0
  91. package/dist/lib/analytics/forwarder.js +109 -0
  92. package/dist/lib/analytics/logs.js +78 -0
  93. package/dist/lib/analytics/metrics.js +78 -0
  94. package/dist/lib/analytics/recorder.js +92 -0
  95. package/dist/lib/analytics/review-analytics.js +75 -0
  96. package/dist/lib/analytics/sequence.js +77 -0
  97. package/dist/lib/analytics/store.js +131 -0
  98. package/dist/lib/analytics/turn-recap.js +279 -0
  99. package/dist/lib/artifact_id.js +108 -0
  100. package/dist/lib/auth-breaker.js +161 -0
  101. package/dist/lib/auto-index.js +112 -0
  102. package/dist/lib/classifier.js +88 -0
  103. package/dist/lib/config.js +298 -0
  104. package/dist/lib/conflict-advisory.js +64 -0
  105. package/dist/lib/debug-bundle.js +520 -0
  106. package/dist/lib/enrichment/ingest.js +301 -0
  107. package/dist/lib/enrichment/plan.js +253 -0
  108. package/dist/lib/enrichment/protocol.js +359 -0
  109. package/dist/lib/enrichment/scout-brief.js +176 -0
  110. package/dist/lib/failure-telemetry.js +444 -0
  111. package/dist/lib/git.js +200 -0
  112. package/dist/lib/governance-cache.js +77 -0
  113. package/dist/lib/governed-path-cache.js +76 -0
  114. package/dist/lib/http.js +677 -0
  115. package/dist/lib/identity-envelope.js +23 -0
  116. package/dist/lib/kb-candidate.js +65 -0
  117. package/dist/lib/kb_acl.js +98 -0
  118. package/dist/lib/login.js +353 -0
  119. package/dist/lib/mcp-fetchers.js +130 -0
  120. package/dist/lib/mcp-restart.js +47 -0
  121. package/dist/lib/observability.js +805 -0
  122. package/dist/lib/open-url.js +33 -0
  123. package/dist/lib/orphan-guard.js +70 -0
  124. package/dist/lib/packaged.js +21 -0
  125. package/dist/lib/reconcile-sessions.js +171 -0
  126. package/dist/lib/redactor.js +89 -0
  127. package/dist/lib/relationship-candidate-query.js +27 -0
  128. package/dist/lib/render.js +611 -0
  129. package/dist/lib/rules/applicability.js +64 -0
  130. package/dist/lib/rules/attest-code-rule-version.js +47 -0
  131. package/dist/lib/rules/attest-notes-location.js +217 -0
  132. package/dist/lib/rules/attest-rule-version.js +69 -0
  133. package/dist/lib/rules/canonical-json.js +97 -0
  134. package/dist/lib/rules/ce0-emit.js +64 -0
  135. package/dist/lib/rules/ce0-evidence.js +281 -0
  136. package/dist/lib/rules/ce0-recall-sample.js +82 -0
  137. package/dist/lib/rules/ce0-rule.js +55 -0
  138. package/dist/lib/rules/ce0-sampling-bucket.js +15 -0
  139. package/dist/lib/rules/ce0-store.js +683 -0
  140. package/dist/lib/rules/ce0-telemetry-project.js +93 -0
  141. package/dist/lib/rules/ce0-telemetry.js +158 -0
  142. package/dist/lib/rules/code-rule-registry.js +17 -0
  143. package/dist/lib/rules/command-match.js +185 -0
  144. package/dist/lib/rules/consult-evidence-binding.js +27 -0
  145. package/dist/lib/rules/consultation-capture-adapter.js +193 -0
  146. package/dist/lib/rules/content-match.js +56 -0
  147. package/dist/lib/rules/deny-admission.js +99 -0
  148. package/dist/lib/rules/durable-observation.js +190 -0
  149. package/dist/lib/rules/enforce-notes-version.js +421 -0
  150. package/dist/lib/rules/evaluation-input-hash.js +126 -0
  151. package/dist/lib/rules/evaluator.js +108 -0
  152. package/dist/lib/rules/inert-rule-families.js +51 -0
  153. package/dist/lib/rules/input-authority-resolver.js +241 -0
  154. package/dist/lib/rules/interception-schema.js +170 -0
  155. package/dist/lib/rules/interception-store.js +267 -0
  156. package/dist/lib/rules/live-input-authority.js +66 -0
  157. package/dist/lib/rules/local-matcher.js +108 -0
  158. package/dist/lib/rules/local-observe.js +79 -0
  159. package/dist/lib/rules/local-rule-version-repo.js +214 -0
  160. package/dist/lib/rules/memory-requirement.js +109 -0
  161. package/dist/lib/rules/notes-observe.js +39 -0
  162. package/dist/lib/rules/notes-path.js +261 -0
  163. package/dist/lib/rules/notes-rule.js +75 -0
  164. package/dist/lib/rules/observe-adapter.js +114 -0
  165. package/dist/lib/rules/observed-rule-hash.js +119 -0
  166. package/dist/lib/rules/prompt-submit-adapter.js +132 -0
  167. package/dist/lib/rules/requirement-subject.js +240 -0
  168. package/dist/lib/rules/rule-activity.js +67 -0
  169. package/dist/lib/rules/rule-version-hash.js +151 -0
  170. package/dist/lib/rules/runtime-scope.js +55 -0
  171. package/dist/lib/rules/stop-adapter.js +116 -0
  172. package/dist/lib/rules/stop-response-snapshot.js +174 -0
  173. package/dist/lib/rules/types.js +10 -0
  174. package/dist/lib/rules/ulid.js +46 -0
  175. package/dist/lib/rules/version-evaluation.js +156 -0
  176. package/dist/lib/scanner/agent-memory.js +99 -0
  177. package/dist/lib/scanner/bootstrap-summary.js +87 -0
  178. package/dist/lib/scanner/cache.js +59 -0
  179. package/dist/lib/scanner/frontmatter.js +42 -0
  180. package/dist/lib/scanner/parse-directives.js +69 -0
  181. package/dist/lib/scanner/parse-structured.js +72 -0
  182. package/dist/lib/scanner/render.js +73 -0
  183. package/dist/lib/scanner/scan.js +132 -0
  184. package/dist/lib/scanner/score.js +38 -0
  185. package/dist/lib/scanner/scout-mission.js +126 -0
  186. package/dist/lib/scanner/types.js +7 -0
  187. package/dist/lib/session-scope.js +195 -0
  188. package/dist/lib/spool.js +355 -0
  189. package/dist/lib/staleness.js +100 -0
  190. package/dist/lib/steer-cache.js +87 -0
  191. package/dist/lib/tagged-reference.js +20 -0
  192. package/dist/lib/temporal.js +109 -0
  193. package/dist/lib/turn-recap-emit.js +67 -0
  194. package/dist/lib/unwire.js +253 -0
  195. package/dist/lib/update-check.js +469 -0
  196. package/dist/lib/update-notifier.js +217 -0
  197. package/dist/lib/upgrade-apply.js +643 -0
  198. package/dist/lib/wire.js +1087 -0
  199. package/dist/lib/workspace.js +96 -0
  200. package/dist/lib/zip.js +154 -0
  201. package/dist/pretool-entry.js +37 -0
  202. package/package.json +75 -0
@@ -0,0 +1,728 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.RULES_PUBLISH_USAGE = exports.RULES_USAGE = void 0;
37
+ exports.runRulesList = runRulesList;
38
+ exports.runRulesActivity = runRulesActivity;
39
+ exports.runRulesAttest = runRulesAttest;
40
+ exports.runRulesRevoke = runRulesRevoke;
41
+ exports.runRulesPublish = runRulesPublish;
42
+ const fs = __importStar(require("fs"));
43
+ const path = __importStar(require("path"));
44
+ const config_1 = require("../lib/config");
45
+ const http_1 = require("../lib/http");
46
+ const attest_notes_location_1 = require("../lib/rules/attest-notes-location");
47
+ const attest_code_rule_version_1 = require("../lib/rules/attest-code-rule-version");
48
+ const attest_rule_version_1 = require("../lib/rules/attest-rule-version");
49
+ const ce0_store_1 = require("../lib/rules/ce0-store");
50
+ const code_rule_registry_1 = require("../lib/rules/code-rule-registry");
51
+ const interception_store_1 = require("../lib/rules/interception-store");
52
+ const local_rule_version_repo_1 = require("../lib/rules/local-rule-version-repo");
53
+ const rule_version_hash_1 = require("../lib/rules/rule-version-hash");
54
+ const rule_activity_1 = require("../lib/rules/rule-activity");
55
+ const runtime_scope_1 = require("../lib/rules/runtime-scope");
56
+ const ulid_1 = require("../lib/rules/ulid");
57
+ const evidence_1 = require("./evidence");
58
+ // `mla rules list`: the read-only window onto what R0 has durably observed in the ACTIVE runtime
59
+ // scope (proposal §10.2 R0 readiness). The runtime hooks only RECORD; this command never mutates a
60
+ // row, never calls the backend, and never crosses into another scope. It lists one entry per distinct
61
+ // observed rule: the observed-rule-v1 hash, the scanned directive text, the latest verdict and when
62
+ // it was last seen, how many times it has been observed, and whether a local attested version derives
63
+ // from that observed hash. It is a thin IO shell over listObservedRulesInScope; the runtime-scope
64
+ // resolver and the store path are injectable so the workflow is testable end to end.
65
+ const USAGE = "usage: mla rules <list|activity|attest|revoke|publish> [...]";
66
+ exports.RULES_USAGE = USAGE;
67
+ const PUBLISH_USAGE = "usage: mla rules publish [--json]";
68
+ exports.RULES_PUBLISH_USAGE = PUBLISH_USAGE;
69
+ const ATTEST_USAGE = "usage: mla rules attest --from-observed <observedRuleHash> " +
70
+ "[--new-rule <ruleId> | --rule <ruleId>] [--agent-on-user-request --yes] [--no-publish]";
71
+ const CODE_RULE_ATTEST_USAGE = "usage: mla rules attest --from-code-rule <name> [--agent-on-user-request --yes] [--no-publish] " +
72
+ "(the rule id is pinned by the registry; --from-observed / --new-rule / --rule are not allowed here)";
73
+ const REVOKE_USAGE = "usage: mla rules revoke [--rule <ruleId>] [--yes] [--no-publish]";
74
+ /** `mla rules list [--json]`: list the observed rules R0 has recorded in the active runtime scope. */
75
+ async function runRulesList(argv, deps = {}) {
76
+ const out = deps.out ?? ((line) => console.log(line));
77
+ const json = argv.includes("--json");
78
+ const resolveScope = deps.resolveRuntimeScopeId ?? runtime_scope_1.resolveActiveRuntimeScopeId;
79
+ const runtimeScopeId = resolveScope(deps.cwd);
80
+ const dbPath = deps.storePath ?? (0, evidence_1.defaultCe0StorePath)();
81
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
82
+ const open = deps.openStore ?? ce0_store_1.openCe0Store;
83
+ const store = open(dbPath);
84
+ try {
85
+ const rules = (0, interception_store_1.listObservedRulesInScope)(store, runtimeScopeId);
86
+ out(json ? JSON.stringify({ runtimeScopeId, rules }) : formatRulesText(runtimeScopeId, rules));
87
+ return 0;
88
+ }
89
+ finally {
90
+ (0, ce0_store_1.closeCe0Store)(store);
91
+ }
92
+ }
93
+ /** Render the listing as a compact, stable text block (one record per observed rule). */
94
+ function formatRulesText(runtimeScopeId, rules) {
95
+ const lines = [`runtime scope: ${runtimeScopeId}`];
96
+ if (rules.length === 0) {
97
+ lines.push("no observed rules recorded in this scope");
98
+ return lines.join("\n");
99
+ }
100
+ lines.push(`${rules.length} observed rule(s)`, "");
101
+ for (const r of rules) {
102
+ lines.push(r.observedRuleHash);
103
+ lines.push(` text: ${r.ruleText}`);
104
+ lines.push(` latest: ${r.latestResult} at ${r.latestObservedAt} (${r.observationCount} observation(s))`);
105
+ lines.push(` version: ${r.hasLocalVersion ? "attested" : "none"}`);
106
+ lines.push("");
107
+ }
108
+ return lines.join("\n").trimEnd();
109
+ }
110
+ // ---------------------------------------------------------------------------
111
+ // mla rules activity: the R2-LOCAL accountability projection (proposal §2.6 / §3.7 "still local")
112
+ // ---------------------------------------------------------------------------
113
+ /**
114
+ * `mla rules activity [--json]`: the §2.6 "observed N, violated M" measurement per LIVE rule in the
115
+ * active scope. This is the SHIPPABLE half of R2: the terminal-outcome half (project a COMMITTED
116
+ * violation, ie "the action the deny named actually happened") is BLOCKED BY DESIGN because the supported
117
+ * PreToolUse payload carries no tool_use_id and heuristic post correlation is forbidden (§9.10, §2.6).
118
+ * The measurement that licenses promoting a rule out of DRY_RUN needs no correlation: it is a pure
119
+ * projection of the records MLA already owns at evaluation time (tool_attempt + rule_evaluation_record),
120
+ * so this command reads them with one local query and never calls the backend or crosses scope. A thin IO
121
+ * shell over summarizeRuleActivity; the runtime-scope resolver and the store path are injectable.
122
+ */
123
+ async function runRulesActivity(argv, deps = {}) {
124
+ const out = deps.out ?? ((line) => console.log(line));
125
+ const json = argv.includes("--json");
126
+ const resolveScope = deps.resolveRuntimeScopeId ?? runtime_scope_1.resolveActiveRuntimeScopeId;
127
+ const runtimeScopeId = resolveScope(deps.cwd);
128
+ const dbPath = deps.storePath ?? (0, evidence_1.defaultCe0StorePath)();
129
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
130
+ const open = deps.openStore ?? ce0_store_1.openCe0Store;
131
+ const store = open(dbPath);
132
+ try {
133
+ const rules = (0, rule_activity_1.summarizeRuleActivity)(store, runtimeScopeId);
134
+ out(json ? JSON.stringify({ runtimeScopeId, rules }) : formatActivityText(runtimeScopeId, rules));
135
+ return 0;
136
+ }
137
+ finally {
138
+ (0, ce0_store_1.closeCe0Store)(store);
139
+ }
140
+ }
141
+ /** Render the per-rule measurement as a compact, stable text block (one record per LIVE rule). */
142
+ function formatActivityText(runtimeScopeId, rules) {
143
+ const lines = [`runtime scope: ${runtimeScopeId}`];
144
+ if (rules.length === 0) {
145
+ lines.push("no LIVE rules attested in this scope");
146
+ return lines.join("\n");
147
+ }
148
+ lines.push(`${rules.length} LIVE rule(s)`, "");
149
+ for (const r of rules) {
150
+ lines.push(`${r.ruleId} (${r.versionId})`);
151
+ lines.push(` observed ${r.observed}, compliant ${r.compliant}, ` +
152
+ `violation ${r.violation}, denied ${r.deniedEmitted}, ` +
153
+ `enforcement-unavailable ${r.enforcementUnavailable}`);
154
+ lines.push("");
155
+ }
156
+ return lines.join("\n").trimEnd();
157
+ }
158
+ /** Read the audited operator from the authenticated session; only a user-token is a human attestor. */
159
+ function defaultResolveOperator() {
160
+ const cfg = (0, config_1.readConfig)();
161
+ if (cfg.auth.mode !== "user-token")
162
+ return null;
163
+ return { attestedBy: cfg.auth.user.id, displayName: cfg.auth.user.displayName || cfg.auth.user.id };
164
+ }
165
+ /** Synchronously read one line of confirmation from stdin (the interactive default). */
166
+ function defaultConfirm(prompt) {
167
+ process.stderr.write(`${prompt} [y/N] `);
168
+ const buf = Buffer.alloc(256);
169
+ try {
170
+ const n = fs.readSync(0, buf, 0, buf.length, null);
171
+ const answer = buf.toString("utf8", 0, n).trim().toLowerCase();
172
+ return answer === "y" || answer === "yes";
173
+ }
174
+ catch {
175
+ return false;
176
+ }
177
+ }
178
+ /** Render the normalized payload P IN FULL so the human confirms the matcher + ceiling, not the prose
179
+ * line alone (proposal lines 2102-2107). */
180
+ function formatAttestPayload(payload, canonicalPayloadHash, ruleId) {
181
+ const a = payload.applicability;
182
+ const matcher = a.mode === "action"
183
+ ? `${a.tools.join(", ")} on ${a.matcher.field}${a.matcher.glob ? ` matching ${a.matcher.glob}` : ""}`
184
+ : "(ambient)";
185
+ return [
186
+ `rule: ${ruleId}`,
187
+ `scope: ${payload.runtimeScopeId}`,
188
+ `hash: ${canonicalPayloadHash}`,
189
+ `text: ${payload.text}`,
190
+ `applies to: ${matcher}`,
191
+ `effect: ${payload.effect}`,
192
+ `strength: ${payload.strength}`,
193
+ `delivery: ${payload.deliveryChannels.join(", ")}`,
194
+ `ceiling: ${payload.enforcementCeiling}`,
195
+ `on failure: ${payload.infrastructureFailurePolicy}`,
196
+ `evaluator: ${payload.compliance.evaluatorContractVersion} / ` +
197
+ `${payload.compliance.matcherSchemaVersion} / ${payload.compliance.pathCanonicalizerVersion}`,
198
+ `forbids: ${payload.compliance.config.forbiddenRootRelativePath}`,
199
+ ].join("\n");
200
+ }
201
+ function parseAttestIdentity(argv) {
202
+ const newRuleIdx = argv.indexOf("--new-rule");
203
+ const ruleIdx = argv.indexOf("--rule");
204
+ const newRuleGiven = newRuleIdx >= 0;
205
+ const ruleGiven = ruleIdx >= 0;
206
+ if (newRuleGiven && ruleGiven)
207
+ return { kind: "usage-error" };
208
+ if (!newRuleGiven && !ruleGiven)
209
+ return { kind: "default" };
210
+ if (newRuleGiven) {
211
+ const id = argv[newRuleIdx + 1];
212
+ if (!id || id.startsWith("--"))
213
+ return { kind: "usage-error" };
214
+ return { kind: "explicit", identity: { mode: "NEW_RULE", ruleId: id } };
215
+ }
216
+ const id = argv[ruleIdx + 1];
217
+ if (!id || id.startsWith("--"))
218
+ return { kind: "usage-error" };
219
+ return { kind: "explicit", identity: { mode: "SUCCESSOR", ruleId: id } };
220
+ }
221
+ /**
222
+ * `mla rules attest --from-observed <observedRuleHash>`: mint the LIVE notes-location version from an
223
+ * R0 observed snapshot (proposal worked attest flow lines 2037-2069). It resolves the EXACT observed
224
+ * snapshot in the active scope (A.3), runs the §2.4 admission gate + conversion (the pure core),
225
+ * resolves the accountable operator from the authenticated session, short-circuits an idempotent
226
+ * re-attest, displays the full normalized payload, selects the attestation method from the terminal /
227
+ * agent flags, and mints (superseding any prior LIVE version) in the A.4 repo's single transaction. It
228
+ * mints the attested DENY ceiling regardless of the runtime deny-admission gates (§10.2): attestation
229
+ * and effective enforcement are separate (lines 2117-2124). A miss at any step mints nothing.
230
+ */
231
+ async function runRulesAttest(argv, deps = {}) {
232
+ const out = deps.out ?? ((line) => console.log(line));
233
+ const err = deps.err ?? ((line) => console.error(line));
234
+ // `--from-code-rule <name>` is a distinct attest source from `--from-observed <hash>`: it mints a LIVE
235
+ // version of a PRODUCT-SHIPPED code rule (e.g. CE0 consult-evidence) rather than an R0 observed snapshot.
236
+ // It is dispatched FIRST and owns the whole invocation; the two sources never combine.
237
+ if (argv.includes("--from-code-rule")) {
238
+ return attestCodeRule(argv, deps, out, err);
239
+ }
240
+ const flagIdx = argv.indexOf("--from-observed");
241
+ const observedRuleHash = flagIdx >= 0 ? argv[flagIdx + 1] : undefined;
242
+ if (!observedRuleHash || observedRuleHash.startsWith("--")) {
243
+ err(ATTEST_USAGE);
244
+ return 2;
245
+ }
246
+ const agentOnUserRequest = argv.includes("--agent-on-user-request");
247
+ const yes = argv.includes("--yes");
248
+ const noPublish = argv.includes("--no-publish");
249
+ const identityChoice = parseAttestIdentity(argv);
250
+ if (identityChoice.kind === "usage-error") {
251
+ err(ATTEST_USAGE);
252
+ return 2;
253
+ }
254
+ const ruleId = identityChoice.kind === "explicit" ? identityChoice.identity.ruleId : attest_notes_location_1.NOTES_LOCATION_RULE_ID;
255
+ const resolveScope = deps.resolveRuntimeScopeId ?? runtime_scope_1.resolveActiveRuntimeScopeId;
256
+ const runtimeScopeId = resolveScope(deps.cwd);
257
+ const dbPath = deps.storePath ?? (0, evidence_1.defaultCe0StorePath)();
258
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
259
+ const open = deps.openStore ?? ce0_store_1.openCe0Store;
260
+ const store = open(dbPath);
261
+ let stateChanged = false;
262
+ try {
263
+ const resolution = (0, interception_store_1.resolveObservedSnapshotInScope)(store, runtimeScopeId, observedRuleHash);
264
+ if (resolution.kind === "NOT_FOUND") {
265
+ err(`no observed rule with hash ${observedRuleHash} in runtime scope ${runtimeScopeId}: ` +
266
+ `not found, nothing to attest`);
267
+ return 1;
268
+ }
269
+ if (resolution.kind === "COLLISION") {
270
+ err(`observed hash ${observedRuleHash} is a collision in scope ${runtimeScopeId}: ` +
271
+ `${resolution.distinctSnapshotCount} distinct snapshots share it; refusing to attest`);
272
+ return 1;
273
+ }
274
+ const conversion = identityChoice.kind === "explicit"
275
+ ? (0, attest_notes_location_1.convertForbiddenRootSnapshot)(resolution.observedRuleSnapshot, runtimeScopeId)
276
+ : (0, attest_notes_location_1.convertNotesLocationSnapshot)(resolution.observedRuleSnapshot, runtimeScopeId);
277
+ if (!conversion.admitted) {
278
+ err(`snapshot is not a supported forbidden-root rule (${conversion.reason}): ${conversion.detail}`);
279
+ return 1;
280
+ }
281
+ const payload = conversion.payload;
282
+ const resolveOperator = deps.resolveOperator ?? defaultResolveOperator;
283
+ const operator = resolveOperator();
284
+ if (!operator) {
285
+ err("refusing to attest: not logged in as a human operator (run `mla login`); " +
286
+ "attestation requires an authenticated MLA operator");
287
+ return 1;
288
+ }
289
+ const canonicalPayloadHash = (0, rule_version_hash_1.ruleVersionHash)(payload);
290
+ // The idempotent re-attest short-circuit is valid ONLY when this attest could legitimately match an
291
+ // existing LIVE version (the notes default or an explicit SUCCESSOR). A NEW_RULE must never no-op on a
292
+ // hash match: an id that is already taken is a COLLISION the mint rejects (P0.55), never a silent reuse.
293
+ const isNewRule = identityChoice.kind === "explicit" && identityChoice.identity.mode === "NEW_RULE";
294
+ const current = (0, local_rule_version_repo_1.getLiveLocalRuleVersion)(store, runtimeScopeId, ruleId);
295
+ if (!isNewRule && current && current.canonicalPayloadHash === canonicalPayloadHash) {
296
+ out(`already attested: LIVE version ${current.versionId} already carries ` +
297
+ `${canonicalPayloadHash}; no-op`);
298
+ return 0;
299
+ }
300
+ out("note: enforcementCeiling is DENY. The live PreToolUse deny is armed: when this version is LIVE and " +
301
+ "the deny-admission gates pass (check `mla doctor`), a VIOLATION is denied on the wire; otherwise it " +
302
+ "degrades to observe-only. Attestation mints the DENY-ceiling authority; effective enforcement is " +
303
+ "the separate runtime decision (§10.2).");
304
+ out(formatAttestPayload(payload, canonicalPayloadHash, ruleId));
305
+ let attestationMethod;
306
+ if (agentOnUserRequest && yes) {
307
+ attestationMethod = "AGENT_ON_USER_REQUEST";
308
+ }
309
+ else if (deps.isInteractive ? deps.isInteractive() : Boolean(process.stdin.isTTY && process.stdout.isTTY)) {
310
+ const confirm = deps.confirm ?? defaultConfirm;
311
+ const ok = await confirm(`Attest this notes-location DENY rule for scope ${runtimeScopeId}?`);
312
+ if (!ok) {
313
+ err("attestation not confirmed; nothing minted");
314
+ return 1;
315
+ }
316
+ attestationMethod = "HUMAN_DIRECT";
317
+ }
318
+ else {
319
+ err("refusing to attest non-interactively without confirmation; pass " +
320
+ "--agent-on-user-request --yes to attest on the operator's explicit instruction");
321
+ return 1;
322
+ }
323
+ const newVersionId = deps.newVersionId ?? (() => `ver:${(0, ulid_1.ulid)()}`);
324
+ const now = deps.now ?? (() => new Date().toISOString());
325
+ let outcome;
326
+ try {
327
+ const mintBase = {
328
+ payload,
329
+ observedRuleHash,
330
+ attestedBy: operator.attestedBy,
331
+ attestationMethod,
332
+ versionId: newVersionId(),
333
+ attestedAt: now(),
334
+ };
335
+ outcome =
336
+ identityChoice.kind === "explicit"
337
+ ? (0, attest_rule_version_1.mintAttestedRuleVersion)(store, { ...mintBase, identity: identityChoice.identity })
338
+ : (0, attest_notes_location_1.mintAttestedNotesLocationVersion)(store, mintBase);
339
+ }
340
+ catch (e) {
341
+ // P0.55 identity faults are operator errors, not crashes: a NEW_RULE collision or a SUCCESSOR with
342
+ // no prior LIVE version mints nothing and exits 1 with the writer's exact, actionable message.
343
+ if (e instanceof attest_rule_version_1.RuleIdentityCollisionError || e instanceof local_rule_version_repo_1.NoLiveVersionToSupersedeError) {
344
+ err(e.message);
345
+ return 1;
346
+ }
347
+ throw e;
348
+ }
349
+ if (outcome.outcome === "SUPERSEDED") {
350
+ out(`SUPERSEDED ${outcome.supersededVersionId} -> MINTED version ${outcome.version.versionId} ` +
351
+ `(${outcome.version.canonicalPayloadHash}) LIVE in scope ${runtimeScopeId}`);
352
+ stateChanged = true;
353
+ }
354
+ else if (outcome.outcome === "MINTED") {
355
+ out(`MINTED version ${outcome.version.versionId} ` +
356
+ `(${outcome.version.canonicalPayloadHash}) LIVE in scope ${runtimeScopeId}`);
357
+ stateChanged = true;
358
+ }
359
+ else {
360
+ out(`already attested: ${outcome.version.versionId}; no-op`);
361
+ }
362
+ }
363
+ finally {
364
+ (0, ce0_store_1.closeCe0Store)(store);
365
+ }
366
+ // A successful mint changed local truth: best-effort project it to control so the console Rules surface
367
+ // stays in sync without a manual `mla rules publish`. The sync NEVER fails the attest (it already
368
+ // committed locally); a missing workspace / logged-out CLI / unreachable backend is reported, not fatal.
369
+ if (stateChanged) {
370
+ await autoPublishAfterMutation(runtimeScopeId, noPublish, deps, out, err);
371
+ }
372
+ return 0;
373
+ }
374
+ /** Render the code-rule's frozen payload IN FULL so the human confirms what identity they are arming. It
375
+ * is RECORD_ONLY by construction (a CE0 forcing function, not a forbidden-root deny), so there is no
376
+ * matcher/effect/ceiling to negotiate: arming changes NO runtime behavior, it only gives the rule's
377
+ * obligations a durable, attested version identity to bind to. */
378
+ function formatCodeRuleAttest(codeRule, runtimeScopeId) {
379
+ return [
380
+ `rule: ${codeRule.ruleId}`,
381
+ `scope: ${runtimeScopeId}`,
382
+ `hash: ${codeRule.canonicalPayloadHash}`,
383
+ `ceiling: RECORD_ONLY (no deny; arming changes no runtime behavior)`,
384
+ `payload: ${codeRule.serializedPayload}`,
385
+ ].join("\n");
386
+ }
387
+ /**
388
+ * `mla rules attest --from-code-rule <name>`: mint the LIVE LocalRuleVersion of a product-shipped code
389
+ * rule (the GAP 3 slice-2 arm that gives the CE0 consult-evidence obligation a durable attested identity to
390
+ * bind to). The logical id is PINNED by the registry (P0.55 is satisfied because no logical-identity choice
391
+ * is being MADE here -- the registry already pinned it), so `--new-rule` / `--rule` and the `--from-observed`
392
+ * source are all rejected as usage errors. NEW_RULE vs SUCCESSOR is auto-derived from store state (a LIVE
393
+ * version present -> SUCCESSOR, else NEW_RULE); a re-attest whose frozen hash matches the LIVE version is an
394
+ * idempotent no-op. The rule is RECORD_ONLY, so arming it is provably inert under the R4 three-class
395
+ * partition (INV-CONFLICT / P0.13): it cannot silently disarm an out-of-family PROHIBIT deny. It reuses the
396
+ * operator resolution, confirmation, and attestation-method selection of the observed arm; a miss at any
397
+ * step mints nothing.
398
+ */
399
+ async function attestCodeRule(argv, deps, out, err) {
400
+ const flagIdx = argv.indexOf("--from-code-rule");
401
+ const name = flagIdx >= 0 ? argv[flagIdx + 1] : undefined;
402
+ if (!name || name.startsWith("--")) {
403
+ err(CODE_RULE_ATTEST_USAGE);
404
+ return 2;
405
+ }
406
+ // A code rule's logical id is pinned by the registry; an operator may not choose it, nor mix in the
407
+ // observed source. These are usage errors (exit 2), distinct from a clean miss that mints nothing.
408
+ if (argv.includes("--from-observed") || argv.includes("--new-rule") || argv.includes("--rule")) {
409
+ err(CODE_RULE_ATTEST_USAGE);
410
+ return 2;
411
+ }
412
+ const agentOnUserRequest = argv.includes("--agent-on-user-request");
413
+ const yes = argv.includes("--yes");
414
+ const noPublish = argv.includes("--no-publish");
415
+ const codeRule = (0, code_rule_registry_1.getCodeRule)(name);
416
+ if (!codeRule) {
417
+ err(`no code rule named '${name}' ships in this build; nothing to attest`);
418
+ return 1;
419
+ }
420
+ const resolveScope = deps.resolveRuntimeScopeId ?? runtime_scope_1.resolveActiveRuntimeScopeId;
421
+ const runtimeScopeId = resolveScope(deps.cwd);
422
+ const dbPath = deps.storePath ?? (0, evidence_1.defaultCe0StorePath)();
423
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
424
+ const open = deps.openStore ?? ce0_store_1.openCe0Store;
425
+ const store = open(dbPath);
426
+ let stateChanged = false;
427
+ try {
428
+ const resolveOperator = deps.resolveOperator ?? defaultResolveOperator;
429
+ const operator = resolveOperator();
430
+ if (!operator) {
431
+ err("refusing to attest: not logged in as a human operator (run `mla login`); " +
432
+ "attestation requires an authenticated MLA operator");
433
+ return 1;
434
+ }
435
+ // The idempotent short-circuit before we display anything or prompt: a re-attest of the same frozen
436
+ // bytes is a clean no-op (the writer would also catch it, but short-circuiting keeps the UX quiet).
437
+ const current = (0, local_rule_version_repo_1.getLiveLocalRuleVersion)(store, runtimeScopeId, codeRule.ruleId);
438
+ if (current && current.canonicalPayloadHash === codeRule.canonicalPayloadHash) {
439
+ out(`already attested: LIVE version ${current.versionId} already carries ` +
440
+ `${codeRule.canonicalPayloadHash}; no-op`);
441
+ return 0;
442
+ }
443
+ out("note: this rule is RECORD_ONLY. Arming it changes NO runtime behavior; it only gives the rule's " +
444
+ "obligations a durable, attested version identity to bind to (the CE0 measurement is identical " +
445
+ "armed or unarmed).");
446
+ out(formatCodeRuleAttest(codeRule, runtimeScopeId));
447
+ let attestationMethod;
448
+ if (agentOnUserRequest && yes) {
449
+ attestationMethod = "AGENT_ON_USER_REQUEST";
450
+ }
451
+ else if (deps.isInteractive ? deps.isInteractive() : Boolean(process.stdin.isTTY && process.stdout.isTTY)) {
452
+ const confirm = deps.confirm ?? defaultConfirm;
453
+ const ok = await confirm(`Attest this RECORD_ONLY code rule ${codeRule.ruleId} for scope ${runtimeScopeId}?`);
454
+ if (!ok) {
455
+ err("attestation not confirmed; nothing minted");
456
+ return 1;
457
+ }
458
+ attestationMethod = "HUMAN_DIRECT";
459
+ }
460
+ else {
461
+ err("refusing to attest non-interactively without confirmation; pass " +
462
+ "--agent-on-user-request --yes to attest on the operator's explicit instruction");
463
+ return 1;
464
+ }
465
+ // NEW_RULE on a fresh id, SUCCESSOR once a LIVE version exists (a seed bump rotated the frozen hash).
466
+ // The registry pins the id, so this is auto-derived, never an operator choice. A re-arm after a revoke
467
+ // (history present, nothing LIVE) surfaces the writer's RuleIdentityCollisionError honestly.
468
+ const mode = current ? "SUCCESSOR" : "NEW_RULE";
469
+ const newVersionId = deps.newVersionId ?? (() => `ver:${(0, ulid_1.ulid)()}`);
470
+ const now = deps.now ?? (() => new Date().toISOString());
471
+ let outcome;
472
+ try {
473
+ outcome = (0, attest_code_rule_version_1.mintAttestedCodeRuleVersion)(store, {
474
+ mode,
475
+ codeRule,
476
+ runtimeScopeId,
477
+ attestedBy: operator.attestedBy,
478
+ attestationMethod,
479
+ versionId: newVersionId(),
480
+ attestedAt: now(),
481
+ });
482
+ }
483
+ catch (e) {
484
+ if (e instanceof attest_rule_version_1.RuleIdentityCollisionError || e instanceof local_rule_version_repo_1.NoLiveVersionToSupersedeError) {
485
+ err(e.message);
486
+ return 1;
487
+ }
488
+ throw e;
489
+ }
490
+ if (outcome.outcome === "SUPERSEDED") {
491
+ out(`SUPERSEDED ${outcome.supersededVersionId} -> MINTED version ${outcome.version.versionId} ` +
492
+ `(${outcome.version.canonicalPayloadHash}) LIVE in scope ${runtimeScopeId}`);
493
+ stateChanged = true;
494
+ }
495
+ else if (outcome.outcome === "MINTED") {
496
+ out(`MINTED version ${outcome.version.versionId} ` +
497
+ `(${outcome.version.canonicalPayloadHash}) LIVE in scope ${runtimeScopeId}`);
498
+ stateChanged = true;
499
+ }
500
+ else {
501
+ out(`already attested: ${outcome.version.versionId}; no-op`);
502
+ }
503
+ }
504
+ finally {
505
+ (0, ce0_store_1.closeCe0Store)(store);
506
+ }
507
+ // A successful arm changed local truth: best-effort project it to control (see runRulesAttest). Sync is
508
+ // never fatal to the attest, which already committed; `--no-publish` opts out for local-only operators.
509
+ if (stateChanged) {
510
+ await autoPublishAfterMutation(runtimeScopeId, noPublish, deps, out, err);
511
+ }
512
+ return 0;
513
+ }
514
+ /**
515
+ * `mla rules revoke [--rule <ruleId>] [--yes]`: the kill switch (the answer to "what's the harm" of
516
+ * wiring the deny live). It flips the current LIVE version of a (scope, rule) to REVOKED in the A.4
517
+ * repo's single transaction. After this the (scope, rule) has NO LIVE version, so the enforce seam
518
+ * finds NO_LIVE_VERSION and fails open: enforcement stops cleanly without deleting any history. The
519
+ * rule defaults to the notes-location pilot; --rule names another logical rule in this scope (the
520
+ * revoke path is rule-agnostic, it needs only a ruleId, never a payload). Disarming governance must be
521
+ * deliberate, so it confirms exactly like attest: an explicit --yes, or an interactive prompt. Pulling
522
+ * an already-pulled switch (nothing LIVE) is an idempotent success, never an error. Best-effort names
523
+ * the operator who pulled it; it never blocks the disarm on being logged in.
524
+ */
525
+ async function runRulesRevoke(argv, deps = {}) {
526
+ const out = deps.out ?? ((line) => console.log(line));
527
+ const err = deps.err ?? ((line) => console.error(line));
528
+ const ruleFlagIdx = argv.indexOf("--rule");
529
+ const ruleId = ruleFlagIdx >= 0 ? argv[ruleFlagIdx + 1] : attest_notes_location_1.NOTES_LOCATION_RULE_ID;
530
+ if (!ruleId || ruleId.startsWith("--")) {
531
+ err(REVOKE_USAGE);
532
+ return 2;
533
+ }
534
+ const yes = argv.includes("--yes");
535
+ const noPublish = argv.includes("--no-publish");
536
+ const resolveScope = deps.resolveRuntimeScopeId ?? runtime_scope_1.resolveActiveRuntimeScopeId;
537
+ const runtimeScopeId = resolveScope(deps.cwd);
538
+ const dbPath = deps.storePath ?? (0, evidence_1.defaultCe0StorePath)();
539
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
540
+ const open = deps.openStore ?? ce0_store_1.openCe0Store;
541
+ const store = open(dbPath);
542
+ let stateChanged = false;
543
+ try {
544
+ const current = (0, local_rule_version_repo_1.getLiveLocalRuleVersion)(store, runtimeScopeId, ruleId);
545
+ if (!current) {
546
+ out(`nothing LIVE to revoke for rule ${ruleId} in scope ${runtimeScopeId}; already disarmed`);
547
+ return 0;
548
+ }
549
+ out(`about to disarm rule ${ruleId} in scope ${runtimeScopeId}: ` +
550
+ `LIVE version ${current.versionId} (${current.canonicalPayloadHash})`);
551
+ if (!yes) {
552
+ const interactive = deps.isInteractive
553
+ ? deps.isInteractive()
554
+ : Boolean(process.stdin.isTTY && process.stdout.isTTY);
555
+ if (!interactive) {
556
+ err("refusing to disarm non-interactively without confirmation; pass --yes to revoke");
557
+ return 1;
558
+ }
559
+ const confirm = deps.confirm ?? defaultConfirm;
560
+ const ok = await confirm(`Revoke (disarm) rule ${ruleId} for scope ${runtimeScopeId}? It will then fail open.`);
561
+ if (!ok) {
562
+ err("revoke not confirmed; the rule stays LIVE");
563
+ return 1;
564
+ }
565
+ }
566
+ const revoked = (0, local_rule_version_repo_1.revokeLiveLocalRuleVersion)(store, runtimeScopeId, ruleId);
567
+ const resolveOperator = deps.resolveOperator ?? defaultResolveOperator;
568
+ const operator = resolveOperator();
569
+ const by = operator ? ` by ${operator.displayName ?? operator.attestedBy}` : "";
570
+ out(`REVOKED version ${revoked.versionId} (${revoked.canonicalPayloadHash})${by}; ` +
571
+ `enforcement disarmed for rule ${ruleId} in scope ${runtimeScopeId}; the rule now fails open`);
572
+ stateChanged = true;
573
+ }
574
+ finally {
575
+ (0, ce0_store_1.closeCe0Store)(store);
576
+ }
577
+ // A revoke drops the rule from the scope's LIVE set: best-effort re-project so control reconciles the
578
+ // now-absent rule to STALE and it leaves the console Active tab. Never fatal to the local disarm.
579
+ if (stateChanged) {
580
+ await autoPublishAfterMutation(runtimeScopeId, noPublish, deps, out, err);
581
+ }
582
+ return 0;
583
+ }
584
+ /** The console renders evidenceJson.statement verbatim as the rule headline; pull the human-readable rule
585
+ * text out of the opaque payload, falling back to the logical id for code rules with no `.text` field. */
586
+ function ruleHeadline(record) {
587
+ try {
588
+ const parsed = JSON.parse(record.rulePayload);
589
+ if (typeof parsed.text === "string" && parsed.text.trim())
590
+ return parsed.text.trim();
591
+ }
592
+ catch {
593
+ // opaque / non-JSON payload (should not happen for a stored version); fall through to the id.
594
+ }
595
+ return record.ruleId;
596
+ }
597
+ /** The default network seam: POST the batch to control with the session bearer the rest of the CLI uses. */
598
+ function defaultPublish(cfg, body) {
599
+ return (0, http_1.post)(cfg, "/internal/v1/relationship-candidates/publish-rules", body, 15000);
600
+ }
601
+ /** Map one LIVE LocalRuleVersion to its control publish item (headline pulled from the opaque payload). */
602
+ function buildPublishItem(r) {
603
+ return {
604
+ ruleId: r.ruleId,
605
+ versionId: r.versionId,
606
+ text: ruleHeadline(r),
607
+ payloadHash: r.canonicalPayloadHash,
608
+ lifecycleStatus: "LIVE",
609
+ attestedBy: r.attestedBy ?? null,
610
+ attestedAt: r.attestedAt ?? null,
611
+ attestationMethod: r.attestationMethod ?? null,
612
+ };
613
+ }
614
+ /**
615
+ * Project the LIVE attested rules in `runtimeScopeId` to control (the exact batch `mla rules publish`
616
+ * sends). It is the single projection path shared by the explicit publish command and the auto-publish
617
+ * hooks on attest/revoke. It NEVER throws: an unbound workspace / logged-out CLI is reported as `skipped`
618
+ * (loadConfig threw before any network call), an unreachable backend as `failed`; only a real POST that
619
+ * returns is `synced`. The store is opened read-only and always closed; an empty LIVE set still posts so a
620
+ * revoked-to-nothing scope reconciles away on the backend.
621
+ */
622
+ async function publishLiveRulesForScope(runtimeScopeId, deps) {
623
+ let cfg;
624
+ try {
625
+ cfg = deps.loadConfig ? deps.loadConfig() : (0, config_1.loadWorkspaceConfig)();
626
+ }
627
+ catch (e) {
628
+ return { kind: "skipped", reason: e.message };
629
+ }
630
+ const dbPath = deps.storePath ?? (0, evidence_1.defaultCe0StorePath)();
631
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
632
+ const open = deps.openStore ?? ce0_store_1.openCe0Store;
633
+ const store = open(dbPath);
634
+ let live;
635
+ try {
636
+ live = (0, local_rule_version_repo_1.listLiveLocalRuleVersions)(store, runtimeScopeId);
637
+ }
638
+ finally {
639
+ (0, ce0_store_1.closeCe0Store)(store);
640
+ }
641
+ const rules = live.map(buildPublishItem);
642
+ const body = { workspaceId: cfg.workspaceId, runtimeScopeId, rules };
643
+ const publish = deps.publish ?? defaultPublish;
644
+ try {
645
+ const result = await publish(cfg, body);
646
+ return { kind: "synced", sent: rules.length, workspaceId: cfg.workspaceId, result };
647
+ }
648
+ catch (e) {
649
+ return { kind: "failed", reason: e.message };
650
+ }
651
+ }
652
+ /**
653
+ * After a state-changing attest/revoke, best-effort sync the scope's LIVE rules into control so the console
654
+ * Rules surface stays current without a manual `mla rules publish`. This is the auto-publish hook: the local
655
+ * mutation has ALREADY committed, so a sync miss is reported (skip/warn) but never changes the command's
656
+ * success. `--no-publish` opts out entirely (offline / local-only operators, or a CI run with no workspace).
657
+ */
658
+ async function autoPublishAfterMutation(runtimeScopeId, noPublish, deps, out, err) {
659
+ if (noPublish) {
660
+ out("note: --no-publish set; skipped the console sync. The change is attested locally; run " +
661
+ "`mla rules publish` to surface it on the console Rules page.");
662
+ return;
663
+ }
664
+ const outcome = await publishLiveRulesForScope(runtimeScopeId, deps);
665
+ if (outcome.kind === "synced") {
666
+ out(`synced to console: workspace ${outcome.workspaceId} now reflects ${outcome.result.published} ` +
667
+ `published, ${outcome.result.retired} retired (from ${outcome.sent} LIVE rule(s) in this scope).`);
668
+ }
669
+ else if (outcome.kind === "skipped") {
670
+ out(`note: skipped console sync (${outcome.reason}). The change is attested locally; run ` +
671
+ "`mla rules publish` once a workspace is bound to surface it.");
672
+ }
673
+ else {
674
+ err(`warning: console sync failed (${outcome.reason}). The change IS attested locally; run ` +
675
+ "`mla rules publish` to retry the sync.");
676
+ }
677
+ }
678
+ /** Render the publish outcome as a compact, stable text block. */
679
+ function formatPublishText(runtimeScopeId, workspaceId, sent, result) {
680
+ const lines = [
681
+ `runtime scope: ${runtimeScopeId}`,
682
+ `workspace: ${workspaceId}`,
683
+ `sent ${sent} LIVE rule(s); published ${result.published}, retired ${result.retired}`,
684
+ ];
685
+ if (result.items.length > 0) {
686
+ lines.push("");
687
+ for (const it of result.items) {
688
+ lines.push(` ${it.action.padEnd(9)} ${it.ruleId} (${it.candidateId})`);
689
+ }
690
+ }
691
+ return lines.join("\n");
692
+ }
693
+ /**
694
+ * `mla rules publish [--json]`: project the LIVE attested rule versions in the ACTIVE runtime scope into
695
+ * control so they surface on the console Rules page. It is the bridge half of the local-first rules engine:
696
+ * `attest` / `revoke` only ever mutate the local CE0 store, and this command is the one place that pushes
697
+ * that local truth to the backend. It reads EVERY LIVE LocalRuleVersion in the scope, maps each to a publish
698
+ * item (the human-readable headline pulled from the opaque payload), and POSTs the whole set to control,
699
+ * which upserts each as an ACCEPTED workspace-scoped rule-kind candidate (idempotent by workspace + ruleId)
700
+ * and reconciles-by-omission: any rule it published from THIS scope before that is no longer LIVE is driven
701
+ * to STALE so a revoked rule disappears from the Active tab. An empty LIVE set is NOT a no-op: it still posts
702
+ * (with the scope) so the last-revoked rule reconciles away. Read-only on the local store; one network call.
703
+ */
704
+ async function runRulesPublish(argv, deps = {}) {
705
+ const out = deps.out ?? ((line) => console.log(line));
706
+ const err = deps.err ?? ((line) => console.error(line));
707
+ const json = argv.includes("--json");
708
+ const resolveScope = deps.resolveRuntimeScopeId ?? runtime_scope_1.resolveActiveRuntimeScopeId;
709
+ const runtimeScopeId = resolveScope(deps.cwd);
710
+ // The explicit command shares the ONE projection path with the auto-publish hooks, but maps the outcome
711
+ // to its own hard exit codes: an unbound workspace is a usage error (2), a failed POST is a failure (1).
712
+ const outcome = await publishLiveRulesForScope(runtimeScopeId, deps);
713
+ if (outcome.kind === "skipped") {
714
+ err(outcome.reason);
715
+ return 2;
716
+ }
717
+ if (outcome.kind === "failed") {
718
+ err(`failed to publish rules to control: ${outcome.reason}`);
719
+ return 1;
720
+ }
721
+ if (json) {
722
+ out(JSON.stringify({ runtimeScopeId, workspaceId: outcome.workspaceId, ...outcome.result }));
723
+ }
724
+ else {
725
+ out(formatPublishText(runtimeScopeId, outcome.workspaceId, outcome.sent, outcome.result));
726
+ }
727
+ return 0;
728
+ }