@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,240 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CONSULTATION_SOURCES = exports.SUBJECT_TERM_OVERLAP_THRESHOLD_VERSION = exports.SUBJECT_TERM_OVERLAP_THRESHOLD = exports.SUBJECT_MATCH_VERSION = exports.SUBJECT_STOPWORDS = exports.SUBJECT_STOPWORD_SET_VERSION = exports.SUBJECT_FINGERPRINT_SCHEMA_VERSION = exports.REQUIREMENT_SUBJECT_EXTRACTOR_VERSION = void 0;
4
+ exports.normalizeSubjectTerms = normalizeSubjectTerms;
5
+ exports.buildRequirementSubjectPayload = buildRequirementSubjectPayload;
6
+ exports.requirementSubjectFingerprint = requirementSubjectFingerprint;
7
+ exports.extractRequirementSubject = extractRequirementSubject;
8
+ exports.buildRequiredSubjectFromPrompt = buildRequiredSubjectFromPrompt;
9
+ exports.buildConsultationSubjectFromQuery = buildConsultationSubjectFromQuery;
10
+ exports.matchConsultationSubject = matchConsultationSubject;
11
+ exports.consultationContributesProof = consultationContributesProof;
12
+ exports.selectEligibleConsultations = selectEligibleConsultations;
13
+ exports.recomputeSubjectSatisfaction = recomputeSubjectSatisfaction;
14
+ exports.isObligationSatisfied = isObligationSatisfied;
15
+ const canonical_json_1 = require("./canonical-json");
16
+ exports.REQUIREMENT_SUBJECT_EXTRACTOR_VERSION = "prompt-terms-v1";
17
+ exports.SUBJECT_FINGERPRINT_SCHEMA_VERSION = "requirement-subject-v1";
18
+ exports.SUBJECT_STOPWORD_SET_VERSION = "seed-v1";
19
+ /** Minimum length for a kept term; drops single-character noise left by punctuation /
20
+ * contraction splits ("don't" -> "don","t" -> "don"). */
21
+ const MIN_TERM_LENGTH = 2;
22
+ const sortedFrozen = (words) => Object.freeze(Array.from(new Set(words)).sort());
23
+ /** The versioned seed stopword set: English function words plus the question and
24
+ * governance scaffolding that wraps a governed subject ("what did we decide about X",
25
+ * "why did we choose Y", "who owns Z", "are we still doing W"). Content nouns that ARE
26
+ * the subject (policy, canonical, architecture, decision, model, ...) are deliberately
27
+ * absent. Frozen so identity is byte-stable; the corpus pins the exact behavior. */
28
+ exports.SUBJECT_STOPWORDS = sortedFrozen([
29
+ "a", "about", "an", "and", "any", "are", "as", "at",
30
+ "be", "been", "being", "between", "but", "by",
31
+ "can", "choose", "chose", "chosen", "could",
32
+ "decide", "decided", "did", "do", "does", "doing",
33
+ "for", "from",
34
+ "had", "has", "have", "how",
35
+ "i", "in", "into", "is", "it", "its",
36
+ "my", "nor", "of", "on", "or", "our", "over",
37
+ "own", "owned", "owns",
38
+ "approve", "approved", "approves",
39
+ "should", "so", "still",
40
+ "that", "the", "their", "them", "then", "there", "these", "this", "those", "to",
41
+ "under", "us",
42
+ "was", "we", "were", "what", "when", "where", "which", "who", "why", "will", "with", "would",
43
+ "you", "your",
44
+ ]);
45
+ const STOPWORD_SET = new Set(exports.SUBJECT_STOPWORDS);
46
+ /**
47
+ * Lift the deterministic subject terms from a prompt: lowercase, split on any
48
+ * non-alphanumeric run, drop stopwords and sub-`MIN_TERM_LENGTH` tokens, then return the
49
+ * sorted, deduped set. Order-independent and idempotent.
50
+ */
51
+ function normalizeSubjectTerms(text) {
52
+ const tokens = text.toLowerCase().split(/[^a-z0-9]+/);
53
+ const kept = new Set();
54
+ for (const tok of tokens) {
55
+ if (tok.length < MIN_TERM_LENGTH)
56
+ continue;
57
+ if (STOPWORD_SET.has(tok))
58
+ continue;
59
+ kept.add(tok);
60
+ }
61
+ return Array.from(kept).sort();
62
+ }
63
+ const sortedUniq = (xs) => Array.from(new Set(xs)).sort();
64
+ /**
65
+ * Build the closed canonical fingerprint payload. Every id / term set is sorted +
66
+ * deduped so the digest is a pure function of SET content (identity), and the schema
67
+ * version is pinned so a version bump rotates every digest.
68
+ */
69
+ function buildRequirementSubjectPayload(fields) {
70
+ return {
71
+ schemaVersion: exports.SUBJECT_FINGERPRINT_SCHEMA_VERSION,
72
+ normalizedTerms: sortedUniq(fields.normalizedTerms),
73
+ entityIds: sortedUniq(fields.entityIds),
74
+ decisionIds: sortedUniq(fields.decisionIds),
75
+ conceptIds: sortedUniq(fields.conceptIds),
76
+ };
77
+ }
78
+ /** Identity fingerprint over the structured fields (sha256 hex). Identity only. */
79
+ function requirementSubjectFingerprint(fields) {
80
+ return (0, canonical_json_1.sha256Hex)((0, canonical_json_1.canonicalize)(buildRequirementSubjectPayload(fields)));
81
+ }
82
+ /**
83
+ * Extract a structured RequirementSubject from a raw prompt. The term set is lifted
84
+ * deterministically here; entity / decision / concept ids are accepted from an upstream
85
+ * resolver (none wired in v1, so they default empty) rather than fabricated. `subjectId`
86
+ * is derived from the identity fingerprint, so the same structured content always yields
87
+ * the same handle.
88
+ */
89
+ function extractRequirementSubject(prompt, resolved = {}) {
90
+ const fields = {
91
+ normalizedTerms: normalizeSubjectTerms(prompt),
92
+ entityIds: sortedUniq(resolved.entityIds ?? []),
93
+ decisionIds: sortedUniq(resolved.decisionIds ?? []),
94
+ conceptIds: sortedUniq(resolved.conceptIds ?? []),
95
+ };
96
+ const fingerprint = requirementSubjectFingerprint(fields);
97
+ return { subjectId: `subj:${fingerprint}`, ...fields, fingerprint };
98
+ }
99
+ /**
100
+ * The obligation's required subject, lifted from the user prompt. A thin, named call
101
+ * site over the single `extractRequirementSubject` normalizer: both the obligation side
102
+ * and the consultation side share ONE normalizer, never a second extractor. It takes no
103
+ * resolved-ids argument by construction, because the proactive-enrich path surfaces no
104
+ * resolved ids at prompt-submit time; a required subject is strictly terms-only and
105
+ * cannot carry fabricated ids.
106
+ */
107
+ function buildRequiredSubjectFromPrompt(prompt) {
108
+ return extractRequirementSubject(prompt, {});
109
+ }
110
+ exports.SUBJECT_MATCH_VERSION = "deterministic-intersection-v1";
111
+ /** Fraction of the REQUIRED subject's terms that a consultation must contain for a
112
+ * term-based FULL match. Directional: it measures whether the consultation COVERS the
113
+ * required subject, not mutual similarity. */
114
+ exports.SUBJECT_TERM_OVERLAP_THRESHOLD = 0.5;
115
+ exports.SUBJECT_TERM_OVERLAP_THRESHOLD_VERSION = "required-containment-half-v1";
116
+ /**
117
+ * A consultation's query subject, lifted from the retrieval query the agent issued. The
118
+ * SAME normalizer as `buildRequiredSubjectFromPrompt`, so identical text yields a
119
+ * byte-identical subject (the two sides can never silently diverge). Resolved ids are
120
+ * admitted ONLY when the call already surfaced them (e.g. a kb_doc_detail citation id);
121
+ * they default empty, never invented here.
122
+ */
123
+ function buildConsultationSubjectFromQuery(query, resolved = {}) {
124
+ return extractRequirementSubject(query, resolved);
125
+ }
126
+ function intersects(a, b) {
127
+ if (a.length === 0 || b.length === 0)
128
+ return false;
129
+ const set = new Set(a);
130
+ return b.some((x) => set.has(x));
131
+ }
132
+ /** Fraction of `required`'s terms present in `consultation` (directional containment).
133
+ * Zero when `required` has no terms (nothing to cover). */
134
+ function requiredTermContainment(required, consultation) {
135
+ if (required.length === 0)
136
+ return 0;
137
+ const set = new Set(consultation);
138
+ const hit = required.reduce((n, t) => (set.has(t) ? n + 1 : n), 0);
139
+ return hit / required.length;
140
+ }
141
+ /**
142
+ * The CE0 deterministic coverage ladder (§1.6). candidateMatch is true iff a non-empty id
143
+ * intersection on entityIds / decisionIds / conceptIds, OR a required-term containment at
144
+ * or above the versioned threshold. Anything uncertain (including a disjoint pair or a
145
+ * degenerate required subject) fails toward silence: result UNKNOWN, candidateMatch false,
146
+ * which neither satisfies nor violates. This grader never asserts PARTIAL or NONE; the
147
+ * crude term test is not entitled to claim non-coverage, so a non-match is UNKNOWN, not NONE.
148
+ *
149
+ * Source-independent by design: a PROACTIVE_PUSH does NOT blanket match. A push is graded
150
+ * by this same per-subject test; it contributes a proof to a required subject only when its
151
+ * query subjects cover that subject, exactly as `recomputeSubjectSatisfaction` attributes proofs.
152
+ */
153
+ function matchConsultationSubject(required, consultation) {
154
+ const idIntersect = intersects(required.entityIds, consultation.entityIds) ||
155
+ intersects(required.decisionIds, consultation.decisionIds) ||
156
+ intersects(required.conceptIds, consultation.conceptIds);
157
+ const termMatch = requiredTermContainment(required.normalizedTerms, consultation.normalizedTerms) >=
158
+ exports.SUBJECT_TERM_OVERLAP_THRESHOLD;
159
+ const candidateMatch = idIntersect || termMatch;
160
+ return {
161
+ subjectId: required.subjectId,
162
+ result: candidateMatch ? "FULL" : "UNKNOWN",
163
+ candidateMatch,
164
+ };
165
+ }
166
+ /** How a governed-memory consultation was initiated. The tuple is the SINGLE source of both
167
+ * the closed value set and the stable sort order the offline projector resolves a finalized
168
+ * obligation's `satisfiedBySources` into. CE0 emits only PROACTIVE_PUSH and AGENT_PULL; today
169
+ * only AGENT_PULL is written (the PostToolUse capture seam). STOP_RECOVERY_PULL is a held seam:
170
+ * it lives in the enum so its ordering is fixed up front, but CE0 (RECORD_ONLY) never writes
171
+ * it. */
172
+ exports.CONSULTATION_SOURCES = ["PROACTIVE_PUSH", "AGENT_PULL", "STOP_RECOVERY_PULL"];
173
+ /**
174
+ * A consultation contributes proofs iff it COMPLETED and its evidence reached the parent
175
+ * answering context. Result (RESULTS_RETURNED vs NO_MATCH) does NOT gate: asking governed
176
+ * memory about a subject and completing, even with zero hits, is a consultation of that
177
+ * subject. FAILED, UNKNOWN, and undelivered consultations never contribute (we cannot
178
+ * attest they consulted).
179
+ */
180
+ function consultationContributesProof(c) {
181
+ return c.execution === "COMPLETE" && c.deliveredToAnsweringContext === true;
182
+ }
183
+ /**
184
+ * The eligible consultation set for an obligation: contributing consultations (above)
185
+ * recorded on or before the claimed deadline. Pass `deadlineOrderingToken = null` while the
186
+ * obligation has no deadline yet (the first Stop has not claimed it), so every contributing
187
+ * consultation is on time. Once the deadline is claimed at orderingToken D, a consultation at
188
+ * orderingToken > D is late: it stays factual telemetry but can never produce an on-time proof.
189
+ */
190
+ function selectEligibleConsultations(consultations, deadlineOrderingToken) {
191
+ return consultations.filter((c) => consultationContributesProof(c) &&
192
+ (deadlineOrderingToken === null || c.orderingToken <= deadlineOrderingToken));
193
+ }
194
+ /**
195
+ * Accumulate one SubjectSatisfactionProof per COVERED required subject across all ELIGIBLE
196
+ * consultations: a required subject is covered iff some eligible consultation's query
197
+ * subjects match it (via `matchConsultationSubject`), and the proof records the consultation
198
+ * that covered it. Two consultations can jointly satisfy two subjects; one consultation can
199
+ * satisfy several. This is NOT a coverage table: there is no per-subject grade, only the
200
+ * presence or absence of a proof.
201
+ *
202
+ * Deterministic and idempotent: consultations are considered in ascending (orderingToken,
203
+ * then consultationId), so the EARLIEST eligible consultation wins a subject's proof
204
+ * regardless of input order, and a duplicated consultation yields the identical proof. Proofs
205
+ * are emitted in `requiredSubjects` order, at most one per subjectId (a subject listed twice
206
+ * still yields one proof). Uncovered required subjects yield no proof.
207
+ */
208
+ function recomputeSubjectSatisfaction(requiredSubjects, eligibleConsultations) {
209
+ const ordered = [...eligibleConsultations].sort((a, b) => a.orderingToken !== b.orderingToken
210
+ ? a.orderingToken - b.orderingToken
211
+ : a.consultationId < b.consultationId
212
+ ? -1
213
+ : a.consultationId > b.consultationId
214
+ ? 1
215
+ : 0);
216
+ const proofs = [];
217
+ const proven = new Set();
218
+ for (const required of requiredSubjects) {
219
+ if (proven.has(required.subjectId))
220
+ continue;
221
+ const hit = ordered.find((c) => c.consultationSubjects.some((qs) => matchConsultationSubject(required, qs).candidateMatch));
222
+ if (hit) {
223
+ proofs.push({ subjectId: required.subjectId, consultationId: hit.consultationId });
224
+ proven.add(required.subjectId);
225
+ }
226
+ }
227
+ return proofs;
228
+ }
229
+ /**
230
+ * SATISFIED iff every required subject has a proof. An empty required set does NOT vacuously
231
+ * satisfy (fail toward silence): satisfaction requires demonstrated consultation of at least
232
+ * one subject. On-time-ness is already enforced by `selectEligibleConsultations`, so a proof
233
+ * set built from eligible consultations is on time by construction.
234
+ */
235
+ function isObligationSatisfied(requiredSubjects, proofs) {
236
+ if (requiredSubjects.length === 0)
237
+ return false;
238
+ const proven = new Set(proofs.map((p) => p.subjectId));
239
+ return requiredSubjects.every((r) => proven.has(r.subjectId));
240
+ }
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ // R2-LOCAL accountability projection: the §2.6 "observed N, violated M" measurement.
3
+ //
4
+ // The terminal-outcome half of R2 (project a COMMITTED violation, ie "the action the deny named
5
+ // actually happened") is BLOCKED BY DESIGN: the supported Claude Code PreToolUse payload carries no
6
+ // tool_use_id (§9.10), and heuristic post correlation by timestamp / tool name / input hash / transcript
7
+ // position is FORBIDDEN because parallel identical calls make it unsound (§2.6, lines 2209-2221). So this
8
+ // module does NOT correlate anything. It projects ONLY the records MLA already owns at the moment it
9
+ // evaluates a rule at PreToolUse: the tool_attempt and the per-rule rule_evaluation_record.
10
+ //
11
+ // That projection is exactly the measurement §2.6 (lines 2224-2231) says licenses promoting a rule out of
12
+ // DRY_RUN: "you cannot justify promoting a rule from DRY_RUN to ask or deny until you can show 'this rule
13
+ // was observed N times and violated M of them.' The violation log IS that measurement." It needs no
14
+ // correlation and is not blocked. §3.7 ("Phase R2 (still local): accountability and instrumentation")
15
+ // scopes it to first-class local violation events and a console summary, which is this.
16
+ //
17
+ // The measurement is keyed on each LIVE version. A version's track record starts when it goes live: when a
18
+ // rule is superseded and re-attested, the new LIVE version's version_id is fresh, so its counts honestly
19
+ // start at zero (the old version's history is not silently inherited). Pure core over a real ce0 store.
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.summarizeRuleActivity = summarizeRuleActivity;
22
+ /**
23
+ * Project the §2.6 observed/violated measurement for every LIVE rule version in `runtimeScopeId`.
24
+ *
25
+ * Each LIVE version is LEFT JOINed to its version-arm evaluations (so a brand-new LIVE rule with no
26
+ * activity still surfaces as an all-zero row, the floor the operator sees), and each evaluation to its
27
+ * attempt (to tell a violation that emitted a deny from one that was only observed). The join keys on
28
+ * rule_version_id, so observed-only arms (rule_version_id IS NULL, recorded for never-attested rules) and
29
+ * superseded versions never inflate a LIVE version's count. Scope-isolated and ordered by ruleId so the
30
+ * result is deterministic across runs.
31
+ */
32
+ function summarizeRuleActivity(store, runtimeScopeId) {
33
+ const rows = store.db
34
+ .prepare(`SELECT
35
+ v.rule_id AS rule_id,
36
+ v.version_id AS version_id,
37
+ COUNT(e.evaluation_id) AS observed,
38
+ COALESCE(SUM(CASE WHEN e.result = 'COMPLIANT' THEN 1 ELSE 0 END), 0) AS compliant,
39
+ COALESCE(SUM(CASE WHEN e.result = 'VIOLATION' THEN 1 ELSE 0 END), 0) AS violation,
40
+ COALESCE(SUM(CASE WHEN e.effective_enforcement = 'DENY'
41
+ AND a.deny_emission_status = 'RESPONSE_EMITTED'
42
+ THEN 1 ELSE 0 END), 0) AS denied_emitted,
43
+ COALESCE(SUM(CASE WHEN e.result = 'VIOLATION'
44
+ AND e.effective_enforcement = 'NONE'
45
+ THEN 1 ELSE 0 END), 0) AS enforcement_unavailable
46
+ FROM local_rule_version v
47
+ LEFT JOIN rule_evaluation_record e
48
+ ON e.rule_version_id = v.version_id
49
+ AND e.runtime_scope_id = v.runtime_scope_id
50
+ LEFT JOIN tool_attempt a
51
+ ON a.attempt_id = e.attempt_id
52
+ AND a.runtime_scope_id = e.runtime_scope_id
53
+ WHERE v.runtime_scope_id = ?
54
+ AND v.lifecycle_status = 'LIVE'
55
+ GROUP BY v.rule_id, v.version_id
56
+ ORDER BY v.rule_id`)
57
+ .all(runtimeScopeId);
58
+ return rows.map((r) => ({
59
+ ruleId: r.rule_id,
60
+ versionId: r.version_id,
61
+ observed: r.observed,
62
+ compliant: r.compliant,
63
+ violation: r.violation,
64
+ deniedEmitted: r.denied_emitted,
65
+ enforcementUnavailable: r.enforcement_unavailable,
66
+ }));
67
+ }
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RuleVersionHashError = exports.RULE_VERSION_HASH_DOMAIN = void 0;
4
+ exports.buildRuleVersionPayload = buildRuleVersionPayload;
5
+ exports.serializeRuleVersion = serializeRuleVersion;
6
+ exports.ruleVersionHash = ruleVersionHash;
7
+ const crypto_1 = require("crypto");
8
+ const canonical_json_1 = require("./canonical-json");
9
+ /**
10
+ * The `rule-version-v1` canonical hash domain (proposal P0.36, sharpened by P0.53; RulePayloadV1
11
+ * at §3.6).
12
+ *
13
+ * It computes the content identity of an ATTESTED rule version. The digest is
14
+ *
15
+ * SHA-256( domainTag || 0x00 || JCS(payload) ) (lowercase hex)
16
+ *
17
+ * over the IMMUTABLE `RulePayloadV1` ONLY. The version ENVELOPE (ruleId, versionId,
18
+ * lifecycleStatus, supersedesVersionId, derivedFromObservedRuleHash, attestedBy, attestedAt,
19
+ * attestationMethod) is deliberately OUTSIDE the hash: a hash cannot include itself, and issuance
20
+ * metadata is not enforcement-relevant (P0.16, P0.54). JCS is the repo's existing RFC 8785
21
+ * canonicalizer (canonical-json.ts); the domain tag + single 0x00 separator (P0.53) guarantee this
22
+ * digest can NEVER collide with an observed rule (`observed-rule-v1`), an action-input snapshot
23
+ * (`evaluation-input-v1`), or any other hashed artifact, even when two bodies are byte-identical.
24
+ * JCS escapes control characters, so the only raw 0x00 byte in the hash input is the separator.
25
+ *
26
+ * `runtimeScopeId` is INSIDE the hash (P0.51): the same rule bound to a different checkout scope is
27
+ * a different payload with a different digest, which is what makes the payload-scope == envelope-scope
28
+ * rule (§3.6) checkable. Because every field except `text`, `applicability`, and the carried
29
+ * `forbiddenRootRelativePath` is fixed by the notes-location pilot contract (§2.4), two operators
30
+ * attesting the same observed snapshot in the same runtime scope mint a byte-identical payload and the
31
+ * same digest.
32
+ *
33
+ * Per-field NFC caveat (honest, P0.53). The vendored JCS primitive applies NFC to EVERY string,
34
+ * while the contract reserves NFC for prose. For the notes-location pilot every non-prose field (the
35
+ * tool names, the effect / strength / ceiling / policy tokens, the version tags, the relative
36
+ * forbidden root, the runtime scope) is ASCII and therefore NFC-stable, so universal NFC is
37
+ * byte-identical to the per-field rule and the golden vectors are contract-correct. A future payload
38
+ * carrying a non-prose field that can hold non-NFC Unicode (for instance a forbidden path with
39
+ * combining marks) must switch to a per-field-NFC encoder; that boundary is recorded in the ledger.
40
+ */
41
+ exports.RULE_VERSION_HASH_DOMAIN = "rule-version-v1";
42
+ /** Thrown when a payload carries a field outside the rule-version-v1 schema (fail-closed). */
43
+ class RuleVersionHashError extends Error {
44
+ constructor(message) {
45
+ super(message);
46
+ this.name = "RuleVersionHashError";
47
+ }
48
+ }
49
+ exports.RuleVersionHashError = RuleVersionHashError;
50
+ // The closed key sets for each object in the payload schema. Unknown fields are an error (P0.53),
51
+ // so a forward-incompatible producer cannot silently mint a hash a consumer would compute
52
+ // differently. `rationale` is the only optional top-level key; it is omitted, never null, when absent.
53
+ const RULE_PAYLOAD_KEYS = new Set([
54
+ "text",
55
+ "rationale",
56
+ "applicability",
57
+ "compliance",
58
+ "effect",
59
+ "strength",
60
+ "deliveryChannels",
61
+ "enforcementCeiling",
62
+ "infrastructureFailurePolicy",
63
+ "runtimeScopeId",
64
+ "payloadSchemaVersion",
65
+ "canonicalSerializationVersion",
66
+ ]);
67
+ const COMPLIANCE_KEYS = new Set([
68
+ "evaluatorContractVersion",
69
+ "matcherSchemaVersion",
70
+ "pathCanonicalizerVersion",
71
+ "config",
72
+ ]);
73
+ const COMPLIANCE_CONFIG_KEYS = new Set(["forbiddenRootRelativePath"]);
74
+ const APPLICABILITY_AMBIENT_KEYS = new Set(["mode"]);
75
+ const APPLICABILITY_ACTION_KEYS = new Set(["mode", "tools", "matcher"]);
76
+ const MATCHER_KEYS = new Set(["field", "glob"]);
77
+ function rejectUnknownKeys(obj, allowed, context) {
78
+ for (const key of Object.keys(obj)) {
79
+ if (!allowed.has(key)) {
80
+ throw new RuleVersionHashError(`unknown field '${key}' in ${context} is outside the ${exports.RULE_VERSION_HASH_DOMAIN} schema`);
81
+ }
82
+ }
83
+ }
84
+ /** Sort + dedupe a SET-valued field by code unit (P0.53). The pilot's tool names and delivery
85
+ * channels are ASCII, so code unit and code point coincide and the order matches JCS key ordering. */
86
+ function sortedDedupedSet(values) {
87
+ return Array.from(new Set(values)).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
88
+ }
89
+ function buildApplicabilityPayload(a) {
90
+ if (a.mode === "ambient") {
91
+ rejectUnknownKeys(a, APPLICABILITY_AMBIENT_KEYS, "applicability(ambient)");
92
+ return { mode: "ambient" };
93
+ }
94
+ rejectUnknownKeys(a, APPLICABILITY_ACTION_KEYS, "applicability(action)");
95
+ rejectUnknownKeys(a.matcher, MATCHER_KEYS, "applicability.matcher");
96
+ const matcher = { field: a.matcher.field };
97
+ if (a.matcher.glob !== undefined) {
98
+ matcher.glob = a.matcher.glob;
99
+ }
100
+ return {
101
+ mode: "action",
102
+ tools: sortedDedupedSet(a.tools),
103
+ matcher,
104
+ };
105
+ }
106
+ /**
107
+ * Build the closed canonical payload for a RulePayloadV1: the exact object that gets canonicalized
108
+ * and hashed. Rejects unknown fields at every level, applies set discipline to deliveryChannels and
109
+ * applicability.tools, and omits an absent rationale (and matcher glob). Field NFC (prose) is applied
110
+ * by the JCS encoder on the way out; see the per-field caveat in the file header.
111
+ */
112
+ function buildRuleVersionPayload(payload) {
113
+ rejectUnknownKeys(payload, RULE_PAYLOAD_KEYS, "rule payload");
114
+ rejectUnknownKeys(payload.compliance, COMPLIANCE_KEYS, "compliance");
115
+ rejectUnknownKeys(payload.compliance.config, COMPLIANCE_CONFIG_KEYS, "compliance.config");
116
+ const base = {
117
+ text: payload.text,
118
+ applicability: buildApplicabilityPayload(payload.applicability),
119
+ compliance: {
120
+ evaluatorContractVersion: payload.compliance.evaluatorContractVersion,
121
+ matcherSchemaVersion: payload.compliance.matcherSchemaVersion,
122
+ pathCanonicalizerVersion: payload.compliance.pathCanonicalizerVersion,
123
+ config: { forbiddenRootRelativePath: payload.compliance.config.forbiddenRootRelativePath },
124
+ },
125
+ effect: payload.effect,
126
+ strength: payload.strength,
127
+ deliveryChannels: sortedDedupedSet(payload.deliveryChannels),
128
+ enforcementCeiling: payload.enforcementCeiling,
129
+ infrastructureFailurePolicy: payload.infrastructureFailurePolicy,
130
+ runtimeScopeId: payload.runtimeScopeId,
131
+ payloadSchemaVersion: payload.payloadSchemaVersion,
132
+ canonicalSerializationVersion: payload.canonicalSerializationVersion,
133
+ };
134
+ // Absent optional: OMIT the key (never null). Present: include verbatim. Key ORDER is irrelevant
135
+ // (JCS sorts), so a conditional spread is the canonical builder, never a mutation.
136
+ return payload.rationale !== undefined ? { ...base, rationale: payload.rationale } : base;
137
+ }
138
+ /** The exact RFC 8785 canonical JSON string that is hashed (UTF-8). Exposed for golden vectors and
139
+ * debugging; the digest is over these bytes prefixed by the domain. */
140
+ function serializeRuleVersion(payload) {
141
+ return (0, canonical_json_1.canonicalize)(buildRuleVersionPayload(payload));
142
+ }
143
+ /** The rule-version-v1 content hash: SHA-256(domainTag || 0x00 || JCS(payload)), lowercase hex. */
144
+ function ruleVersionHash(payload) {
145
+ const jcs = serializeRuleVersion(payload);
146
+ const h = (0, crypto_1.createHash)("sha256");
147
+ h.update(exports.RULE_VERSION_HASH_DOMAIN, "utf8");
148
+ h.update(Buffer.from([0x00]));
149
+ h.update(jcs, "utf8");
150
+ return h.digest("hex");
151
+ }
@@ -0,0 +1,55 @@
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.resolveActiveRuntimeScopeId = resolveActiveRuntimeScopeId;
37
+ const fs = __importStar(require("fs"));
38
+ const wire_1 = require("../wire");
39
+ // The active runtime scope id (proposal §2.3 / §10.1, P0.51 / decision 7). Every local interception
40
+ // row, the tool attempts, the evaluation records, and the attested versions, is keyed by
41
+ // runtime_scope_id, NEVER by a bare workspaceId. The id is the realpath-resolved checkout root of the
42
+ // activated runtime project: from the working directory, walk to the repo root and canonicalize. For
43
+ // R0/R1 there is NO runtime-scope table (decision 2); the resolved path string IS the identity, so a
44
+ // read or write derives it deterministically from the cwd rather than reading a row. resolveProjectRoot
45
+ // performs the git-toplevel walk (falling back to the cwd outside a repo); realpath then canonicalizes
46
+ // it so worktrees and symlinked paths resolve to one stable identity.
47
+ function resolveActiveRuntimeScopeId(cwd) {
48
+ const root = (0, wire_1.resolveProjectRoot)(cwd);
49
+ try {
50
+ return fs.realpathSync(root);
51
+ }
52
+ catch {
53
+ return root;
54
+ }
55
+ }
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseStopInput = parseStopInput;
4
+ exports.observeStop = observeStop;
5
+ const ce0_store_1 = require("./ce0-store");
6
+ const stop_response_snapshot_1 = require("./stop-response-snapshot");
7
+ function isPlainObject(value) {
8
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9
+ }
10
+ function describeError(err) {
11
+ return err instanceof Error ? err.message : String(err);
12
+ }
13
+ /**
14
+ * Parse the raw Stop payload. Accepts either an already parsed object or the raw JSON string.
15
+ * A Stop has no required payload field beyond being an object, so the only null case is a shape
16
+ * that is not a plain object (or an unparseable string); the adapter maps that null to INFRA.
17
+ * The session coordinate and transcript_path are read but not required here, so their absence gets a
18
+ * distinct diagnostic / disposition at the adapter rather than collapsing into "malformed".
19
+ */
20
+ function parseStopInput(raw) {
21
+ let obj = raw;
22
+ if (typeof raw === "string") {
23
+ try {
24
+ obj = JSON.parse(raw);
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ }
30
+ if (!isPlainObject(obj))
31
+ return null;
32
+ const session = obj.session_id;
33
+ const transcript = obj.transcript_path;
34
+ return {
35
+ session_id: typeof session === "string" ? session : undefined,
36
+ transcript_path: typeof transcript === "string" ? transcript : undefined,
37
+ };
38
+ }
39
+ /** Map a Stage A deadline-claim result onto the adapter's Stage A outcome. */
40
+ function stageAOutcome(result, identity) {
41
+ switch (result.status) {
42
+ case "NO_OBLIGATION":
43
+ return { kind: "NOT_APPLICABLE" };
44
+ case "CLAIMED":
45
+ return {
46
+ kind: "CLAIMED",
47
+ obligationId: result.claim.obligationId,
48
+ localTurnSequence: identity.localTurnSequence,
49
+ deadlineClaimedAt: result.claim.deadlineClaimedAt,
50
+ deadlineClaimedVersion: result.claim.deadlineClaimedVersion,
51
+ stateVersion: result.claim.stateVersion,
52
+ };
53
+ case "ALREADY_CLAIMED":
54
+ return {
55
+ kind: "ALREADY_CLAIMED",
56
+ obligationId: result.claim.obligationId,
57
+ localTurnSequence: identity.localTurnSequence,
58
+ deadlineClaimedAt: result.claim.deadlineClaimedAt,
59
+ };
60
+ }
61
+ }
62
+ /**
63
+ * Stage B: best-effort, outside the Stage A transaction. Read the transcript snapshot and, when it
64
+ * succeeds, record the response pair idempotently onto the turn's assessment. readStopResponseSnapshot
65
+ * never throws; a transcript failure resolves to UNLABELABLE with a stable reason and the snapshot
66
+ * fields stay null. A Stage B store-write fault is the same DB-fault class Stage A already surfaces as
67
+ * INFRA, so it is left to propagate to the adapter's outer catch rather than masked here.
68
+ */
69
+ function recordStageB(store, identity, transcriptPath) {
70
+ const snap = (0, stop_response_snapshot_1.readStopResponseSnapshot)(transcriptPath);
71
+ if (!snap.ok)
72
+ return { kind: "UNLABELABLE", reason: snap.reason };
73
+ const written = (0, ce0_store_1.recordStopResponseSnapshot)(store, identity, {
74
+ responseHash: snap.responseHash,
75
+ responseSourceRef: snap.responseSourceRef,
76
+ });
77
+ return { kind: written.status };
78
+ }
79
+ /**
80
+ * Observe one Stop. Resolves the turn's LocalTurnIdentity, runs Stage A (freeze the obligation's
81
+ * eligibility boundary and stamp the observation) and Stage B (best-effort response snapshot), and
82
+ * returns an empty (injection-free) hook response.
83
+ */
84
+ function observeStop(rawInput, config) {
85
+ const NO_INJECTION = {};
86
+ const now = config.now ?? Date.now;
87
+ const parsed = parseStopInput(rawInput);
88
+ if (!parsed) {
89
+ return { response: NO_INJECTION, outcome: { kind: "INFRA", diagnostic: "malformed hook input" } };
90
+ }
91
+ if (!parsed.session_id) {
92
+ return {
93
+ response: NO_INJECTION,
94
+ outcome: { kind: "INFRA", diagnostic: "missing session_id coordinate" },
95
+ };
96
+ }
97
+ try {
98
+ const identity = (0, ce0_store_1.resolveLatestTurnIdentity)(config.store, {
99
+ workspaceId: config.workspaceId,
100
+ sessionId: parsed.session_id,
101
+ });
102
+ if (!identity) {
103
+ return { response: NO_INJECTION, outcome: { kind: "NOT_APPLICABLE" } };
104
+ }
105
+ const claim = (0, ce0_store_1.claimFirstStop)(config.store, identity, config.ruleVersionId, now);
106
+ const outcome = stageAOutcome(claim, identity);
107
+ const snapshot = recordStageB(config.store, identity, parsed.transcript_path);
108
+ return { response: NO_INJECTION, outcome, snapshot };
109
+ }
110
+ catch (err) {
111
+ return {
112
+ response: NO_INJECTION,
113
+ outcome: { kind: "INFRA", diagnostic: `persistence failure: ${describeError(err)}` },
114
+ };
115
+ }
116
+ }