@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.
- package/LICENSE +201 -0
- package/README.md +81 -0
- package/dist/build-info.json +9 -0
- package/dist/bundles/ask-core.js +396 -0
- package/dist/bundles/mcp.js +16592 -0
- package/dist/bundles/trace-core.js +263 -0
- package/dist/cli.js +828 -0
- package/dist/commands/activate.js +781 -0
- package/dist/commands/adoption.js +130 -0
- package/dist/commands/ask.js +290 -0
- package/dist/commands/context.js +114 -0
- package/dist/commands/debug.js +313 -0
- package/dist/commands/doctor.js +1021 -0
- package/dist/commands/enrich.js +427 -0
- package/dist/commands/evidence.js +229 -0
- package/dist/commands/flush.js +184 -0
- package/dist/commands/graph.js +104 -0
- package/dist/commands/init.js +272 -0
- package/dist/commands/internal-active-review.js +322 -0
- package/dist/commands/internal-auto-index.js +188 -0
- package/dist/commands/internal-capture-decisions.js +320 -0
- package/dist/commands/internal-evidence-correlate.js +239 -0
- package/dist/commands/internal-evidence-hooks.js +240 -0
- package/dist/commands/internal-evidence-inject.js +231 -0
- package/dist/commands/internal-finalize.js +221 -0
- package/dist/commands/internal-pretool-observe.js +225 -0
- package/dist/commands/internal-refresh.js +136 -0
- package/dist/commands/internal-session-nudge.js +120 -0
- package/dist/commands/internal-steer-sync.js +117 -0
- package/dist/commands/internal-turn-recap.js +140 -0
- package/dist/commands/kb.js +375 -0
- package/dist/commands/kb_add.js +681 -0
- package/dist/commands/kb_forget.js +283 -0
- package/dist/commands/kb_move.js +45 -0
- package/dist/commands/kb_pending.js +410 -0
- package/dist/commands/kb_personal.js +149 -0
- package/dist/commands/kb_promote.js +188 -0
- package/dist/commands/kb_purge.js +168 -0
- package/dist/commands/kb_reingest.js +335 -0
- package/dist/commands/kb_retime.js +170 -0
- package/dist/commands/kb_review.js +391 -0
- package/dist/commands/kb_revision.js +179 -0
- package/dist/commands/kb_show.js +385 -0
- package/dist/commands/label.js +226 -0
- package/dist/commands/login.js +295 -0
- package/dist/commands/logout.js +108 -0
- package/dist/commands/mcp-supervisor.js +93 -0
- package/dist/commands/mcp.js +227 -0
- package/dist/commands/queue-prune.js +98 -0
- package/dist/commands/review.js +358 -0
- package/dist/commands/rewire.js +124 -0
- package/dist/commands/rules.js +728 -0
- package/dist/commands/scan-context.js +67 -0
- package/dist/commands/session.js +347 -0
- package/dist/commands/stats.js +479 -0
- package/dist/commands/status.js +61 -0
- package/dist/commands/summary.js +250 -0
- package/dist/commands/turn.js +114 -0
- package/dist/commands/uninstall.js +222 -0
- package/dist/commands/whoami.js +102 -0
- package/dist/commands/workspace.js +130 -0
- package/dist/hooks-template/ce0-post-tool-use.sh +34 -0
- package/dist/hooks-template/ce0-session-start.sh +49 -0
- package/dist/hooks-template/ce0-stop.sh +29 -0
- package/dist/hooks-template/ce0-user-prompt-submit.sh +38 -0
- package/dist/hooks-template/common.sh +934 -0
- package/dist/hooks-template/event-batch-filter.jq +67 -0
- package/dist/hooks-template/flush.sh +503 -0
- package/dist/hooks-template/post-tool-use.sh +423 -0
- package/dist/hooks-template/pre-tool-use.sh +69 -0
- package/dist/hooks-template/session-start.sh +140 -0
- package/dist/hooks-template/stop.sh +308 -0
- package/dist/hooks-template/user-prompt-submit.sh +1162 -0
- package/dist/lib/activation.js +79 -0
- package/dist/lib/active-conflict-cache.js +141 -0
- package/dist/lib/active-memory.js +59 -0
- package/dist/lib/active-review-runner.js +26 -0
- package/dist/lib/agent-decision/index.js +25 -0
- package/dist/lib/agent-decision/keys.js +49 -0
- package/dist/lib/agent-decision/normalize-claude.js +183 -0
- package/dist/lib/agent-decision/types.js +21 -0
- package/dist/lib/agent-decision/validate.js +216 -0
- package/dist/lib/analytics/capture.js +96 -0
- package/dist/lib/analytics/command-event.js +267 -0
- package/dist/lib/analytics/consent.js +58 -0
- package/dist/lib/analytics/coverage-gap.js +96 -0
- package/dist/lib/analytics/envelope.js +236 -0
- package/dist/lib/analytics/event-id.js +86 -0
- package/dist/lib/analytics/evidence.js +150 -0
- package/dist/lib/analytics/followthrough.js +194 -0
- package/dist/lib/analytics/forwarder.js +109 -0
- package/dist/lib/analytics/logs.js +78 -0
- package/dist/lib/analytics/metrics.js +78 -0
- package/dist/lib/analytics/recorder.js +92 -0
- package/dist/lib/analytics/review-analytics.js +75 -0
- package/dist/lib/analytics/sequence.js +77 -0
- package/dist/lib/analytics/store.js +131 -0
- package/dist/lib/analytics/turn-recap.js +279 -0
- package/dist/lib/artifact_id.js +108 -0
- package/dist/lib/auth-breaker.js +161 -0
- package/dist/lib/auto-index.js +112 -0
- package/dist/lib/classifier.js +88 -0
- package/dist/lib/config.js +298 -0
- package/dist/lib/conflict-advisory.js +64 -0
- package/dist/lib/debug-bundle.js +520 -0
- package/dist/lib/enrichment/ingest.js +301 -0
- package/dist/lib/enrichment/plan.js +253 -0
- package/dist/lib/enrichment/protocol.js +359 -0
- package/dist/lib/enrichment/scout-brief.js +176 -0
- package/dist/lib/failure-telemetry.js +444 -0
- package/dist/lib/git.js +200 -0
- package/dist/lib/governance-cache.js +77 -0
- package/dist/lib/governed-path-cache.js +76 -0
- package/dist/lib/http.js +677 -0
- package/dist/lib/identity-envelope.js +23 -0
- package/dist/lib/kb-candidate.js +65 -0
- package/dist/lib/kb_acl.js +98 -0
- package/dist/lib/login.js +353 -0
- package/dist/lib/mcp-fetchers.js +130 -0
- package/dist/lib/mcp-restart.js +47 -0
- package/dist/lib/observability.js +805 -0
- package/dist/lib/open-url.js +33 -0
- package/dist/lib/orphan-guard.js +70 -0
- package/dist/lib/packaged.js +21 -0
- package/dist/lib/reconcile-sessions.js +171 -0
- package/dist/lib/redactor.js +89 -0
- package/dist/lib/relationship-candidate-query.js +27 -0
- package/dist/lib/render.js +611 -0
- package/dist/lib/rules/applicability.js +64 -0
- package/dist/lib/rules/attest-code-rule-version.js +47 -0
- package/dist/lib/rules/attest-notes-location.js +217 -0
- package/dist/lib/rules/attest-rule-version.js +69 -0
- package/dist/lib/rules/canonical-json.js +97 -0
- package/dist/lib/rules/ce0-emit.js +64 -0
- package/dist/lib/rules/ce0-evidence.js +281 -0
- package/dist/lib/rules/ce0-recall-sample.js +82 -0
- package/dist/lib/rules/ce0-rule.js +55 -0
- package/dist/lib/rules/ce0-sampling-bucket.js +15 -0
- package/dist/lib/rules/ce0-store.js +683 -0
- package/dist/lib/rules/ce0-telemetry-project.js +93 -0
- package/dist/lib/rules/ce0-telemetry.js +158 -0
- package/dist/lib/rules/code-rule-registry.js +17 -0
- package/dist/lib/rules/command-match.js +185 -0
- package/dist/lib/rules/consult-evidence-binding.js +27 -0
- package/dist/lib/rules/consultation-capture-adapter.js +193 -0
- package/dist/lib/rules/content-match.js +56 -0
- package/dist/lib/rules/deny-admission.js +99 -0
- package/dist/lib/rules/durable-observation.js +190 -0
- package/dist/lib/rules/enforce-notes-version.js +421 -0
- package/dist/lib/rules/evaluation-input-hash.js +126 -0
- package/dist/lib/rules/evaluator.js +108 -0
- package/dist/lib/rules/inert-rule-families.js +51 -0
- package/dist/lib/rules/input-authority-resolver.js +241 -0
- package/dist/lib/rules/interception-schema.js +170 -0
- package/dist/lib/rules/interception-store.js +267 -0
- package/dist/lib/rules/live-input-authority.js +66 -0
- package/dist/lib/rules/local-matcher.js +108 -0
- package/dist/lib/rules/local-observe.js +79 -0
- package/dist/lib/rules/local-rule-version-repo.js +214 -0
- package/dist/lib/rules/memory-requirement.js +109 -0
- package/dist/lib/rules/notes-observe.js +39 -0
- package/dist/lib/rules/notes-path.js +261 -0
- package/dist/lib/rules/notes-rule.js +75 -0
- package/dist/lib/rules/observe-adapter.js +114 -0
- package/dist/lib/rules/observed-rule-hash.js +119 -0
- package/dist/lib/rules/prompt-submit-adapter.js +132 -0
- package/dist/lib/rules/requirement-subject.js +240 -0
- package/dist/lib/rules/rule-activity.js +67 -0
- package/dist/lib/rules/rule-version-hash.js +151 -0
- package/dist/lib/rules/runtime-scope.js +55 -0
- package/dist/lib/rules/stop-adapter.js +116 -0
- package/dist/lib/rules/stop-response-snapshot.js +174 -0
- package/dist/lib/rules/types.js +10 -0
- package/dist/lib/rules/ulid.js +46 -0
- package/dist/lib/rules/version-evaluation.js +156 -0
- package/dist/lib/scanner/agent-memory.js +99 -0
- package/dist/lib/scanner/bootstrap-summary.js +87 -0
- package/dist/lib/scanner/cache.js +59 -0
- package/dist/lib/scanner/frontmatter.js +42 -0
- package/dist/lib/scanner/parse-directives.js +69 -0
- package/dist/lib/scanner/parse-structured.js +72 -0
- package/dist/lib/scanner/render.js +73 -0
- package/dist/lib/scanner/scan.js +132 -0
- package/dist/lib/scanner/score.js +38 -0
- package/dist/lib/scanner/scout-mission.js +126 -0
- package/dist/lib/scanner/types.js +7 -0
- package/dist/lib/session-scope.js +195 -0
- package/dist/lib/spool.js +355 -0
- package/dist/lib/staleness.js +100 -0
- package/dist/lib/steer-cache.js +87 -0
- package/dist/lib/tagged-reference.js +20 -0
- package/dist/lib/temporal.js +109 -0
- package/dist/lib/turn-recap-emit.js +67 -0
- package/dist/lib/unwire.js +253 -0
- package/dist/lib/update-check.js +469 -0
- package/dist/lib/update-notifier.js +217 -0
- package/dist/lib/upgrade-apply.js +643 -0
- package/dist/lib/wire.js +1087 -0
- package/dist/lib/workspace.js +96 -0
- package/dist/lib/zip.js +154 -0
- package/dist/pretool-entry.js +37 -0
- package/package.json +75 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mintAttestedCodeRuleVersion = mintAttestedCodeRuleVersion;
|
|
4
|
+
const attest_rule_version_1 = require("./attest-rule-version");
|
|
5
|
+
const local_rule_version_repo_1 = require("./local-rule-version-repo");
|
|
6
|
+
/**
|
|
7
|
+
* Mint the LIVE version of a code-defined rule. NEW_RULE refuses an id already present in the scope (a
|
|
8
|
+
* collision is an operator error, never a silent re-version); SUCCESSOR supersedes the prior LIVE in the
|
|
9
|
+
* repo's single transaction, and a re-attest whose frozen hash already matches the LIVE version is an
|
|
10
|
+
* idempotent no-op. The stored bytes and hash are the registry's, verbatim; lineage to an observed
|
|
11
|
+
* snapshot is null because a code rule is authored, not observed.
|
|
12
|
+
*/
|
|
13
|
+
function mintAttestedCodeRuleVersion(store, input) {
|
|
14
|
+
const ruleId = input.codeRule.ruleId;
|
|
15
|
+
const scope = input.runtimeScopeId;
|
|
16
|
+
const canonicalPayloadHash = input.codeRule.canonicalPayloadHash;
|
|
17
|
+
const buildRecord = (supersedesVersionId) => ({
|
|
18
|
+
versionId: input.versionId,
|
|
19
|
+
ruleId,
|
|
20
|
+
runtimeScopeId: scope,
|
|
21
|
+
rulePayload: input.codeRule.serializedPayload,
|
|
22
|
+
canonicalPayloadHash,
|
|
23
|
+
lifecycleStatus: "LIVE",
|
|
24
|
+
attestationMethod: input.attestationMethod,
|
|
25
|
+
attestedBy: input.attestedBy,
|
|
26
|
+
supersedesVersionId,
|
|
27
|
+
derivedFromObservedHash: null,
|
|
28
|
+
attestedAt: input.attestedAt,
|
|
29
|
+
});
|
|
30
|
+
if (input.mode === "NEW_RULE") {
|
|
31
|
+
if ((0, local_rule_version_repo_1.listLocalRuleVersionHistory)(store, scope, ruleId).length > 0) {
|
|
32
|
+
throw new attest_rule_version_1.RuleIdentityCollisionError(scope, ruleId);
|
|
33
|
+
}
|
|
34
|
+
const record = buildRecord(null);
|
|
35
|
+
(0, local_rule_version_repo_1.insertLocalRuleVersion)(store, record);
|
|
36
|
+
return { outcome: "MINTED", version: record };
|
|
37
|
+
}
|
|
38
|
+
const current = (0, local_rule_version_repo_1.getLiveLocalRuleVersion)(store, scope, ruleId);
|
|
39
|
+
if (!current) {
|
|
40
|
+
throw new local_rule_version_repo_1.NoLiveVersionToSupersedeError(scope, ruleId);
|
|
41
|
+
}
|
|
42
|
+
if (current.canonicalPayloadHash === canonicalPayloadHash) {
|
|
43
|
+
return { outcome: "NOOP_IDEMPOTENT", version: current };
|
|
44
|
+
}
|
|
45
|
+
const minted = (0, local_rule_version_repo_1.supersedeLiveLocalRuleVersion)(store, buildRecord(current.versionId));
|
|
46
|
+
return { outcome: "SUPERSEDED", version: minted, supersededVersionId: current.versionId };
|
|
47
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NOTES_LOCATION_RULE_ID = void 0;
|
|
4
|
+
exports.convertForbiddenRootSnapshot = convertForbiddenRootSnapshot;
|
|
5
|
+
exports.convertNotesLocationSnapshot = convertNotesLocationSnapshot;
|
|
6
|
+
exports.mintAttestedNotesLocationVersion = mintAttestedNotesLocationVersion;
|
|
7
|
+
const local_rule_version_repo_1 = require("./local-rule-version-repo");
|
|
8
|
+
const attest_rule_version_1 = require("./attest-rule-version");
|
|
9
|
+
// Slice 7 (Phase B.7): the ObservedRuleSpec -> RulePayloadV1 conversion, the §2.4 pilot admission
|
|
10
|
+
// gate, and the mint-or-supersede orchestration behind `mla rules attest --from-observed <hash>`
|
|
11
|
+
// (proposal §2.4 conversion table lines 921-945, the admission gate lines 2076-2085, the worked
|
|
12
|
+
// attest flow lines 2037-2069). This module is the PURE core: it never reads the config, never
|
|
13
|
+
// prompts, never resolves the runtime scope, and never reads the observed snapshot from the store
|
|
14
|
+
// (that is the A.3 resolver). It converts the four observed fields plus the active runtime scope into
|
|
15
|
+
// the frozen RulePayloadV1, and it drives the A.4 repo's one-LIVE-per-(scope, rule) supersession. The
|
|
16
|
+
// command shell (commands/rules.ts) supplies the operator identity, the confirmation, and the IO.
|
|
17
|
+
/** R1 has exactly one logical pilot rule; the identity is FIXED, never chosen (proposal §2.4, P0.55). */
|
|
18
|
+
exports.NOTES_LOCATION_RULE_ID = "notes-location-v1";
|
|
19
|
+
// The fields of the pilot payload that are FIXED by the §2.4 contract (everything except text,
|
|
20
|
+
// applicability, and the carried forbidden root). Naming them as constants makes the conversion table
|
|
21
|
+
// auditable against the proposal line by line.
|
|
22
|
+
const PILOT_EFFECT = "PROHIBIT";
|
|
23
|
+
const PILOT_STRENGTH = "MUST_FOLLOW";
|
|
24
|
+
const PILOT_DELIVERY_CHANNELS = ["preToolUse"];
|
|
25
|
+
const PILOT_ENFORCEMENT_CEILING = "DENY";
|
|
26
|
+
const PILOT_INFRASTRUCTURE_FAILURE_POLICY = "PASS_WITH_ALERT";
|
|
27
|
+
const PILOT_EVALUATOR_CONTRACT_VERSION = "four-state-evaluator-v1";
|
|
28
|
+
const PILOT_MATCHER_SCHEMA_VERSION = "action-applicability-v1";
|
|
29
|
+
const PILOT_PATH_CANONICALIZER_VERSION = "notes-path-v1";
|
|
30
|
+
const PILOT_FORBIDDEN_ROOT = "notes";
|
|
31
|
+
const PILOT_PAYLOAD_SCHEMA_VERSION = "rule-payload-v1";
|
|
32
|
+
const PILOT_CANONICAL_SERIALIZATION_VERSION = "v1";
|
|
33
|
+
// The supported tool SET for the pilot: EXACTLY {Write, Edit} (admission gate condition 2).
|
|
34
|
+
const PILOT_TOOLS = ["Edit", "Write"];
|
|
35
|
+
// The closed observed-rule-v1 schema key sets (must match observed-rule-hash.ts). An out-of-schema
|
|
36
|
+
// field already fails the observed hash (P0.53), so it can never be admitted into the pilot either.
|
|
37
|
+
const OBSERVED_RULE_KEYS = new Set(["text", "applicability", "effect", "forbiddenRootRelativePath"]);
|
|
38
|
+
const APPLICABILITY_AMBIENT_KEYS = new Set(["mode"]);
|
|
39
|
+
const APPLICABILITY_ACTION_KEYS = new Set(["mode", "tools", "matcher"]);
|
|
40
|
+
const MATCHER_KEYS = new Set(["field", "glob"]);
|
|
41
|
+
function reject(reason, detail) {
|
|
42
|
+
return { admitted: false, reason, detail };
|
|
43
|
+
}
|
|
44
|
+
/** Strictly parse a snapshot string into the closed observed-rule-v1 shape, or a typed refusal. */
|
|
45
|
+
function parseObservedSnapshot(snapshotJson) {
|
|
46
|
+
let raw;
|
|
47
|
+
try {
|
|
48
|
+
raw = JSON.parse(snapshotJson);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return reject("SNAPSHOT_UNPARSEABLE", "the observed snapshot is not valid JSON");
|
|
52
|
+
}
|
|
53
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
54
|
+
return reject("MALFORMED_SNAPSHOT", "the observed snapshot is not a JSON object");
|
|
55
|
+
}
|
|
56
|
+
const obj = raw;
|
|
57
|
+
const unknownTop = Object.keys(obj).find((k) => !OBSERVED_RULE_KEYS.has(k));
|
|
58
|
+
if (unknownTop) {
|
|
59
|
+
return reject("UNKNOWN_FIELD", `unknown field '${unknownTop}' is outside the observed-rule-v1 schema`);
|
|
60
|
+
}
|
|
61
|
+
if (typeof obj.text !== "string")
|
|
62
|
+
return reject("MALFORMED_SNAPSHOT", "text must be a string");
|
|
63
|
+
if (typeof obj.effect !== "string")
|
|
64
|
+
return reject("MALFORMED_SNAPSHOT", "effect must be a string");
|
|
65
|
+
if (typeof obj.forbiddenRootRelativePath !== "string") {
|
|
66
|
+
return reject("MALFORMED_SNAPSHOT", "forbiddenRootRelativePath must be a string");
|
|
67
|
+
}
|
|
68
|
+
const applicability = parseApplicability(obj.applicability);
|
|
69
|
+
if ("admitted" in applicability)
|
|
70
|
+
return applicability;
|
|
71
|
+
return {
|
|
72
|
+
ok: true,
|
|
73
|
+
spec: {
|
|
74
|
+
text: obj.text,
|
|
75
|
+
applicability: applicability.value,
|
|
76
|
+
effect: obj.effect,
|
|
77
|
+
forbiddenRootRelativePath: obj.forbiddenRootRelativePath,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function parseApplicability(raw) {
|
|
82
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
83
|
+
return reject("MALFORMED_SNAPSHOT", "applicability must be a JSON object");
|
|
84
|
+
}
|
|
85
|
+
const a = raw;
|
|
86
|
+
if (a.mode === "ambient") {
|
|
87
|
+
const unknown = Object.keys(a).find((k) => !APPLICABILITY_AMBIENT_KEYS.has(k));
|
|
88
|
+
if (unknown)
|
|
89
|
+
return reject("UNKNOWN_FIELD", `unknown field '${unknown}' in applicability(ambient)`);
|
|
90
|
+
return { value: { mode: "ambient" } };
|
|
91
|
+
}
|
|
92
|
+
if (a.mode !== "action") {
|
|
93
|
+
return reject("NOT_ACTION_SCOPED", `applicability.mode '${String(a.mode)}' is not 'action'`);
|
|
94
|
+
}
|
|
95
|
+
const unknown = Object.keys(a).find((k) => !APPLICABILITY_ACTION_KEYS.has(k));
|
|
96
|
+
if (unknown)
|
|
97
|
+
return reject("UNKNOWN_FIELD", `unknown field '${unknown}' in applicability(action)`);
|
|
98
|
+
if (!Array.isArray(a.tools) || !a.tools.every((t) => typeof t === "string")) {
|
|
99
|
+
return reject("MALFORMED_SNAPSHOT", "applicability.tools must be a string array");
|
|
100
|
+
}
|
|
101
|
+
const matcher = parseMatcher(a.matcher);
|
|
102
|
+
if ("admitted" in matcher)
|
|
103
|
+
return matcher;
|
|
104
|
+
return { value: { mode: "action", tools: a.tools, matcher: matcher.value } };
|
|
105
|
+
}
|
|
106
|
+
function parseMatcher(raw) {
|
|
107
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
108
|
+
return reject("MALFORMED_SNAPSHOT", "applicability.matcher must be a JSON object");
|
|
109
|
+
}
|
|
110
|
+
const m = raw;
|
|
111
|
+
const unknown = Object.keys(m).find((k) => !MATCHER_KEYS.has(k));
|
|
112
|
+
if (unknown)
|
|
113
|
+
return reject("UNKNOWN_FIELD", `unknown field '${unknown}' in applicability.matcher`);
|
|
114
|
+
if (typeof m.field !== "string")
|
|
115
|
+
return reject("MALFORMED_SNAPSHOT", "matcher.field must be a string");
|
|
116
|
+
if (m.glob !== undefined && typeof m.glob !== "string") {
|
|
117
|
+
return reject("MALFORMED_SNAPSHOT", "matcher.glob must be a string when present");
|
|
118
|
+
}
|
|
119
|
+
const value = { field: m.field };
|
|
120
|
+
if (m.glob !== undefined)
|
|
121
|
+
value.glob = m.glob;
|
|
122
|
+
return { value };
|
|
123
|
+
}
|
|
124
|
+
function isExactlyWriteEdit(tools) {
|
|
125
|
+
const set = new Set(tools);
|
|
126
|
+
return set.size === PILOT_TOOLS.length && PILOT_TOOLS.every((t) => set.has(t));
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Convert one frozen observed-rule-v1 snapshot into the immutable RulePayloadV1, for the WHOLE PROHIBIT
|
|
130
|
+
* forbidden-root family, or REFUSE it. The admission gate runs BEFORE any payload is built: a snapshot
|
|
131
|
+
* that is not an action-scoped, exactly-{Write, Edit}, field/glob-matcher, effect-PROHIBIT rule with a
|
|
132
|
+
* non-empty forbidden root (closed schema) is never converted, so it can never mint a version.
|
|
133
|
+
*
|
|
134
|
+
* Generalizing the forbidden root beyond the notes pilot is provably conflict-free (proposal §2.0: a
|
|
135
|
+
* conflict requires a LIVE rule whose effect EFFECTIVELY REQUIRES the action that another PROHIBITs;
|
|
136
|
+
* a PROHIBIT rule never requires a write, so two members of this family can never conflict). The
|
|
137
|
+
* evaluator/matcher/canonicalizer contract versions and the path canonicalizer (`notes-path.ts`) are
|
|
138
|
+
* already generic over the configured root, so the same frozen payload shape serves any root. Anything
|
|
139
|
+
* OUTSIDE this family (a different evaluator, PERMIT/EXCLUSION effects, ambient rules) stays R4.
|
|
140
|
+
*
|
|
141
|
+
* The conversion itself is the §2.4 table: text/applicability verbatim, every other field fixed by the
|
|
142
|
+
* family contract, the forbidden root carried AS CONTENT (P0.63), the runtime scope bound from the
|
|
143
|
+
* active evaluation scope. The compliance version triple is supported by construction (the minimal
|
|
144
|
+
* observed spec carries no compliance spec), satisfying gate condition 6.
|
|
145
|
+
*/
|
|
146
|
+
function convertForbiddenRootSnapshot(snapshotJson, runtimeScopeId) {
|
|
147
|
+
const parsed = parseObservedSnapshot(snapshotJson);
|
|
148
|
+
if ("admitted" in parsed)
|
|
149
|
+
return parsed;
|
|
150
|
+
const spec = parsed.spec;
|
|
151
|
+
if (spec.applicability.mode !== "action") {
|
|
152
|
+
return reject("NOT_ACTION_SCOPED", "an ambient rule is never an action-gating version");
|
|
153
|
+
}
|
|
154
|
+
if (!isExactlyWriteEdit(spec.applicability.tools)) {
|
|
155
|
+
return reject("TOOLS_NOT_WRITE_EDIT", `tools must be exactly {Write, Edit}, got [${spec.applicability.tools.join(", ")}]`);
|
|
156
|
+
}
|
|
157
|
+
if (spec.effect !== PILOT_EFFECT) {
|
|
158
|
+
return reject("EFFECT_NOT_PROHIBIT", `effect '${spec.effect}' is not 'PROHIBIT'`);
|
|
159
|
+
}
|
|
160
|
+
if (spec.forbiddenRootRelativePath.trim() === "") {
|
|
161
|
+
return reject("FORBIDDEN_ROOT_EMPTY", "forbidden root is empty: a rule forbidding the repo root is nonsensical");
|
|
162
|
+
}
|
|
163
|
+
const compliance = {
|
|
164
|
+
evaluatorContractVersion: PILOT_EVALUATOR_CONTRACT_VERSION,
|
|
165
|
+
matcherSchemaVersion: PILOT_MATCHER_SCHEMA_VERSION,
|
|
166
|
+
pathCanonicalizerVersion: PILOT_PATH_CANONICALIZER_VERSION,
|
|
167
|
+
config: { forbiddenRootRelativePath: spec.forbiddenRootRelativePath },
|
|
168
|
+
};
|
|
169
|
+
const payload = {
|
|
170
|
+
text: spec.text,
|
|
171
|
+
applicability: spec.applicability,
|
|
172
|
+
compliance,
|
|
173
|
+
effect: PILOT_EFFECT,
|
|
174
|
+
strength: PILOT_STRENGTH,
|
|
175
|
+
deliveryChannels: PILOT_DELIVERY_CHANNELS,
|
|
176
|
+
enforcementCeiling: PILOT_ENFORCEMENT_CEILING,
|
|
177
|
+
infrastructureFailurePolicy: PILOT_INFRASTRUCTURE_FAILURE_POLICY,
|
|
178
|
+
runtimeScopeId,
|
|
179
|
+
payloadSchemaVersion: PILOT_PAYLOAD_SCHEMA_VERSION,
|
|
180
|
+
canonicalSerializationVersion: PILOT_CANONICAL_SERIALIZATION_VERSION,
|
|
181
|
+
};
|
|
182
|
+
return { admitted: true, payload };
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* The notes-location pilot member of the forbidden-root family: the generic converter pinned to the
|
|
186
|
+
* "notes" root (proposal §2.4 condition 5). This is the converter the no-flag `mla rules attest` default
|
|
187
|
+
* uses, preserving the exact armed R1 behavior. An explicit `--new-rule`/`--rule` identity uses the
|
|
188
|
+
* generic `convertForbiddenRootSnapshot` instead.
|
|
189
|
+
*/
|
|
190
|
+
function convertNotesLocationSnapshot(snapshotJson, runtimeScopeId) {
|
|
191
|
+
const generic = convertForbiddenRootSnapshot(snapshotJson, runtimeScopeId);
|
|
192
|
+
if (!generic.admitted)
|
|
193
|
+
return generic;
|
|
194
|
+
const root = generic.payload.compliance.config.forbiddenRootRelativePath;
|
|
195
|
+
if (root !== PILOT_FORBIDDEN_ROOT) {
|
|
196
|
+
return reject("FORBIDDEN_ROOT_UNSUPPORTED", `forbidden root '${root}' is not 'notes'`);
|
|
197
|
+
}
|
|
198
|
+
return generic;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Mint the LIVE notes-location version for the payload's runtime scope (proposal worked attest flow
|
|
202
|
+
* lines 2058-2068) by deferring to the canonical R1 writer. The pilot's logical identity is FIXED to
|
|
203
|
+
* notes-location-v1, but it still flows through the P0.55 identity choice rather than re-deriving it:
|
|
204
|
+
* the FIRST attest in a scope mints that id as a NEW rule (no predecessor), and every later attest is a
|
|
205
|
+
* SUCCESSOR of the prior LIVE version of the same rule (idempotent no-op when the payload hash is
|
|
206
|
+
* unchanged, otherwise a supersession with backward lineage). Serialization, the rule-version-v1 hash,
|
|
207
|
+
* the one-LIVE supersession transaction, and the at-the-attested-ceiling guarantee all live in the
|
|
208
|
+
* canonical writer; attestation and effective enforcement stay separate (§10.2, lines 2117-2124).
|
|
209
|
+
*/
|
|
210
|
+
function mintAttestedNotesLocationVersion(store, input) {
|
|
211
|
+
const scope = input.payload.runtimeScopeId;
|
|
212
|
+
const live = (0, local_rule_version_repo_1.getLiveLocalRuleVersion)(store, scope, exports.NOTES_LOCATION_RULE_ID);
|
|
213
|
+
const identity = live
|
|
214
|
+
? { mode: "SUCCESSOR", ruleId: exports.NOTES_LOCATION_RULE_ID }
|
|
215
|
+
: { mode: "NEW_RULE", ruleId: exports.NOTES_LOCATION_RULE_ID };
|
|
216
|
+
return (0, attest_rule_version_1.mintAttestedRuleVersion)(store, { ...input, identity });
|
|
217
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RuleIdentityCollisionError = void 0;
|
|
4
|
+
exports.mintAttestedRuleVersion = mintAttestedRuleVersion;
|
|
5
|
+
const local_rule_version_repo_1 = require("./local-rule-version-repo");
|
|
6
|
+
const rule_version_hash_1 = require("./rule-version-hash");
|
|
7
|
+
/**
|
|
8
|
+
* Raised when `--new-rule` names a logical id that already exists in the scope. P0.55 requires an
|
|
9
|
+
* accidental collision to be rejected rather than silently versioning the wrong rule; the operator must
|
|
10
|
+
* either choose `--rule <id>` to version the existing rule deliberately, or pick a fresh id.
|
|
11
|
+
*/
|
|
12
|
+
class RuleIdentityCollisionError extends Error {
|
|
13
|
+
runtimeScopeId;
|
|
14
|
+
ruleId;
|
|
15
|
+
constructor(runtimeScopeId, ruleId) {
|
|
16
|
+
super(`cannot mint a NEW rule '${ruleId}' in scope ${runtimeScopeId}: a version with that logical id ` +
|
|
17
|
+
`already exists; use --rule ${ruleId} to version it deliberately, or choose a fresh id`);
|
|
18
|
+
this.runtimeScopeId = runtimeScopeId;
|
|
19
|
+
this.ruleId = ruleId;
|
|
20
|
+
this.name = "RuleIdentityCollisionError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
exports.RuleIdentityCollisionError = RuleIdentityCollisionError;
|
|
24
|
+
/**
|
|
25
|
+
* Mint the LIVE version the operator's identity choice selects (proposal §2.4, P0.55). For NEW_RULE the
|
|
26
|
+
* fresh logical id must not collide with any existing version in the scope (rejected, not silently
|
|
27
|
+
* versioned). For SUCCESSOR the prior LIVE version of the named rule is superseded in the A.4 repo's
|
|
28
|
+
* single BEGIN IMMEDIATE transaction (supersede-first so the one-LIVE partial unique index never trips),
|
|
29
|
+
* and a re-attest whose hash already matches that LIVE version is an idempotent no-op (P1.3).
|
|
30
|
+
*/
|
|
31
|
+
function mintAttestedRuleVersion(store, input) {
|
|
32
|
+
const scope = input.payload.runtimeScopeId;
|
|
33
|
+
const ruleId = input.identity.ruleId;
|
|
34
|
+
const canonicalPayloadHash = (0, rule_version_hash_1.ruleVersionHash)(input.payload);
|
|
35
|
+
const buildRecord = (supersedesVersionId) => ({
|
|
36
|
+
versionId: input.versionId,
|
|
37
|
+
ruleId,
|
|
38
|
+
runtimeScopeId: scope,
|
|
39
|
+
rulePayload: (0, rule_version_hash_1.serializeRuleVersion)(input.payload),
|
|
40
|
+
canonicalPayloadHash,
|
|
41
|
+
lifecycleStatus: "LIVE",
|
|
42
|
+
attestationMethod: input.attestationMethod,
|
|
43
|
+
attestedBy: input.attestedBy,
|
|
44
|
+
supersedesVersionId,
|
|
45
|
+
derivedFromObservedHash: input.observedRuleHash,
|
|
46
|
+
attestedAt: input.attestedAt,
|
|
47
|
+
});
|
|
48
|
+
if (input.identity.mode === "NEW_RULE") {
|
|
49
|
+
// A NEW logical rule must occupy a free id: any version (any lifecycle) under this (scope, ruleId)
|
|
50
|
+
// means the id is already taken, so minting "new" here would silently version the wrong rule.
|
|
51
|
+
if ((0, local_rule_version_repo_1.listLocalRuleVersionHistory)(store, scope, ruleId).length > 0) {
|
|
52
|
+
throw new RuleIdentityCollisionError(scope, ruleId);
|
|
53
|
+
}
|
|
54
|
+
const record = buildRecord(null);
|
|
55
|
+
(0, local_rule_version_repo_1.insertLocalRuleVersion)(store, record);
|
|
56
|
+
return { outcome: "MINTED", version: record };
|
|
57
|
+
}
|
|
58
|
+
// SUCCESSOR: declare this version the successor of an existing logical rule's prior LIVE version.
|
|
59
|
+
const current = (0, local_rule_version_repo_1.getLiveLocalRuleVersion)(store, scope, ruleId);
|
|
60
|
+
if (!current) {
|
|
61
|
+
// A successor needs a prior LIVE version to point back at; there is none for this rule.
|
|
62
|
+
throw new local_rule_version_repo_1.NoLiveVersionToSupersedeError(scope, ruleId);
|
|
63
|
+
}
|
|
64
|
+
if (current.canonicalPayloadHash === canonicalPayloadHash) {
|
|
65
|
+
return { outcome: "NOOP_IDEMPOTENT", version: current };
|
|
66
|
+
}
|
|
67
|
+
const minted = (0, local_rule_version_repo_1.supersedeLiveLocalRuleVersion)(store, buildRecord(current.versionId));
|
|
68
|
+
return { outcome: "SUPERSEDED", version: minted, supersededVersionId: current.versionId };
|
|
69
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CanonicalJsonError = void 0;
|
|
4
|
+
exports.canonicalize = canonicalize;
|
|
5
|
+
exports.sha256Hex = sha256Hex;
|
|
6
|
+
exports.canonicalDigest = canonicalDigest;
|
|
7
|
+
const crypto_1 = require("crypto");
|
|
8
|
+
const MAX_SAFE = Number.MAX_SAFE_INTEGER; // 2^53 - 1
|
|
9
|
+
/** Serialize a single number per the V1 safe-integer profile (see file header). */
|
|
10
|
+
function encodeNumber(n) {
|
|
11
|
+
if (!Number.isFinite(n)) {
|
|
12
|
+
throw new CanonicalJsonError(`non-finite number ${String(n)} cannot be canonically encoded`);
|
|
13
|
+
}
|
|
14
|
+
if (!Number.isInteger(n)) {
|
|
15
|
+
throw new CanonicalJsonError(`non-integer number ${n} rejected by the V1 safe-integer profile; ` +
|
|
16
|
+
`floats are out of scope for fingerprint payloads (see canonical-json.ts header)`);
|
|
17
|
+
}
|
|
18
|
+
if (Math.abs(n) > MAX_SAFE) {
|
|
19
|
+
throw new CanonicalJsonError(`integer ${n} exceeds Number.MAX_SAFE_INTEGER and cannot be encoded losslessly`);
|
|
20
|
+
}
|
|
21
|
+
// String(-0) === "0", String(42) === "42": exactly the RFC 8785 integer form.
|
|
22
|
+
return String(n);
|
|
23
|
+
}
|
|
24
|
+
/** Serialize a string: NFC normalize, then escape exactly as JSON.stringify. */
|
|
25
|
+
function encodeString(s) {
|
|
26
|
+
return JSON.stringify(s.normalize("NFC"));
|
|
27
|
+
}
|
|
28
|
+
function encodeValue(value) {
|
|
29
|
+
if (value === undefined) {
|
|
30
|
+
throw new CanonicalJsonError("undefined is not a canonical value (use null, or omit the object key)");
|
|
31
|
+
}
|
|
32
|
+
if (value === null)
|
|
33
|
+
return "null";
|
|
34
|
+
switch (typeof value) {
|
|
35
|
+
case "boolean":
|
|
36
|
+
return value ? "true" : "false";
|
|
37
|
+
case "number":
|
|
38
|
+
return encodeNumber(value);
|
|
39
|
+
case "string":
|
|
40
|
+
return encodeString(value);
|
|
41
|
+
case "object":
|
|
42
|
+
break;
|
|
43
|
+
default:
|
|
44
|
+
throw new CanonicalJsonError(`unsupported value type '${typeof value}' in canonical payload`);
|
|
45
|
+
}
|
|
46
|
+
if (Array.isArray(value)) {
|
|
47
|
+
return `[${value.map((el) => encodeValue(el)).join(",")}]`;
|
|
48
|
+
}
|
|
49
|
+
return encodeObject(value);
|
|
50
|
+
}
|
|
51
|
+
function encodeObject(obj) {
|
|
52
|
+
// NFC-normalize keys first so ordering is over the normalized form, then sort by
|
|
53
|
+
// UTF-16 code units. JS default string `<` compares by UTF-16 code unit, which is
|
|
54
|
+
// exactly RFC 8785's key order.
|
|
55
|
+
const entries = [];
|
|
56
|
+
const seen = new Map(); // normalizedKey -> originalKey
|
|
57
|
+
for (const rawKey of Object.keys(obj)) {
|
|
58
|
+
const v = obj[rawKey];
|
|
59
|
+
if (v === undefined)
|
|
60
|
+
continue; // absent optional: omit, matches JSON.stringify
|
|
61
|
+
const key = rawKey.normalize("NFC");
|
|
62
|
+
const prior = seen.get(key);
|
|
63
|
+
if (prior !== undefined) {
|
|
64
|
+
throw new CanonicalJsonError(`object keys '${prior}' and '${rawKey}' collide after NFC normalization`);
|
|
65
|
+
}
|
|
66
|
+
seen.set(key, rawKey);
|
|
67
|
+
entries.push([key, v]);
|
|
68
|
+
}
|
|
69
|
+
entries.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
70
|
+
const body = entries
|
|
71
|
+
.map(([k, v]) => `${encodeString(k)}:${encodeValue(v)}`)
|
|
72
|
+
.join(",");
|
|
73
|
+
return `{${body}}`;
|
|
74
|
+
}
|
|
75
|
+
/** Thrown for any input the canonical encoder refuses (fail-closed). */
|
|
76
|
+
class CanonicalJsonError extends Error {
|
|
77
|
+
constructor(message) {
|
|
78
|
+
super(message);
|
|
79
|
+
this.name = "CanonicalJsonError";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
exports.CanonicalJsonError = CanonicalJsonError;
|
|
83
|
+
/**
|
|
84
|
+
* Encode a value to its RFC 8785 canonical JSON string. The returned string, UTF-8
|
|
85
|
+
* encoded, is the exact byte sequence that is hashed.
|
|
86
|
+
*/
|
|
87
|
+
function canonicalize(value) {
|
|
88
|
+
return encodeValue(value);
|
|
89
|
+
}
|
|
90
|
+
/** SHA-256 (hex, lowercase, 64 chars) over the UTF-8 bytes of `text`. */
|
|
91
|
+
function sha256Hex(text) {
|
|
92
|
+
return (0, crypto_1.createHash)("sha256").update(text, "utf8").digest("hex");
|
|
93
|
+
}
|
|
94
|
+
/** Canonical digest: `sha256Hex(canonicalize(value))`. */
|
|
95
|
+
function canonicalDigest(value) {
|
|
96
|
+
return sha256Hex(canonicalize(value));
|
|
97
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// CE0 live telemetry sink (notes/20260617-evidence-consultation-forcing-function-proposal.md §6.4 P0.2):
|
|
3
|
+
// the fail-soft local-append seam between the CE0 hooks and the existing generic analytics spool. The
|
|
4
|
+
// pure ce0-telemetry builders produce a RecordInput; this attaches the run-context envelope and appends
|
|
5
|
+
// it to the local jsonl via the shared recorder.
|
|
6
|
+
//
|
|
7
|
+
// Two §6.4 P0.2 invariants this seam upholds:
|
|
8
|
+
// - Local-append-only: the hook NEVER makes a synchronous network call. recordAnalyticsEvent appends
|
|
9
|
+
// to the local spool and buffers for the existing detached forward; remote delivery is that path's
|
|
10
|
+
// job, so CE0 adds no worker, queue, or uploader.
|
|
11
|
+
// - Fail-soft: a missing run context (no joinable trace/run) or a spool append fault is swallowed and
|
|
12
|
+
// never throws into the turn the hook observed. The durable CE0 store write already happened; live
|
|
13
|
+
// telemetry is strictly best-effort on top of it.
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.emitCe0Event = emitCe0Event;
|
|
16
|
+
const config_1 = require("../config");
|
|
17
|
+
const store_1 = require("../analytics/store");
|
|
18
|
+
const observability_1 = require("../observability");
|
|
19
|
+
const recorder_1 = require("../analytics/recorder");
|
|
20
|
+
/**
|
|
21
|
+
* Append a built CE0 RecordInput to the local analytics spool under a "hook" run-context envelope.
|
|
22
|
+
* Fail-soft and local-append-only (§6.4 P0.2): if there is no joinable run/trace context the event is
|
|
23
|
+
* skipped (the durable store already recorded the fact), and any fault in building or appending is
|
|
24
|
+
* swallowed so a telemetry failure never disturbs the turn.
|
|
25
|
+
*/
|
|
26
|
+
function emitCe0Event(input, coords, deps = {}) {
|
|
27
|
+
try {
|
|
28
|
+
const traceId = deps.traceId ?? (0, observability_1.getRunTraceId)();
|
|
29
|
+
const runId = deps.runId ?? (0, observability_1.getRunId)();
|
|
30
|
+
// No joinable envelope -> a local line that can never join the enrichment is worse than none.
|
|
31
|
+
if (!traceId || !runId)
|
|
32
|
+
return;
|
|
33
|
+
const readCfg = deps.readCfg ??
|
|
34
|
+
(() => {
|
|
35
|
+
try {
|
|
36
|
+
return (0, config_1.readConfig)();
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
const cfg = readCfg();
|
|
43
|
+
const mId = (deps.machineId ?? store_1.machineId)();
|
|
44
|
+
const ctx = {
|
|
45
|
+
workspaceId: coords.workspaceId,
|
|
46
|
+
sessionId: coords.sessionId,
|
|
47
|
+
// The hook cannot cheaply resolve the actor cuid; prefer the configured actor when present, else
|
|
48
|
+
// the hashed machine id (a workspace-scoped anonymous id, never end-user PII).
|
|
49
|
+
distinctId: cfg?.actorUserId ?? mId,
|
|
50
|
+
runId,
|
|
51
|
+
traceId,
|
|
52
|
+
source: "hook",
|
|
53
|
+
now: new Date(coords.nowMs).toISOString(),
|
|
54
|
+
};
|
|
55
|
+
const record = deps.record ?? recorder_1.recordAnalyticsEvent;
|
|
56
|
+
// The onError swallows a local spool append fault; the outer try swallows a buildEvent throw.
|
|
57
|
+
record(ctx, input, deps.env ?? process.env, () => {
|
|
58
|
+
/* fail-soft: a CE0 telemetry append must never escalate into a blocking hook. */
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Fail-soft (§6.4 P0.2): CE0 telemetry must never disturb the turn it observed.
|
|
63
|
+
}
|
|
64
|
+
}
|