@stalfh233/omc-cli 0.0.0-stage → 0.4.10
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 +21 -0
- package/README.md +293 -3
- package/coverage/m1-coverage-manifest-v1.json +1278 -0
- package/dist/approval-token.js +102 -0
- package/dist/args.js +25 -0
- package/dist/artifacts.js +119 -0
- package/dist/bench/call-face-eval.js +256 -0
- package/dist/bench/context-attribution.js +151 -0
- package/dist/bench/discovery-cost-eval.js +230 -0
- package/dist/bench/driver.js +81 -0
- package/dist/bench/evals.js +236 -0
- package/dist/bench/fake-http-server.js +65 -0
- package/dist/bench/instrument.js +87 -0
- package/dist/bench/intent-face-eval.js +343 -0
- package/dist/bench/run.js +166 -0
- package/dist/bench/scenario.js +343 -0
- package/dist/bench/types.js +76 -0
- package/dist/bi-wire.js +41 -0
- package/dist/bizservice-config.js +581 -0
- package/dist/call.js +153 -0
- package/dist/capability-absences.js +23 -0
- package/dist/capability-overview.js +492 -0
- package/dist/capability-shape.js +154 -0
- package/dist/cli-contract.js +70 -0
- package/dist/cli-output.js +73 -0
- package/dist/cli.js +1418 -0
- package/dist/code-rules.js +69 -0
- package/dist/command-transport.js +155 -0
- package/dist/config-store.js +195 -0
- package/dist/context.js +21 -0
- package/dist/contract-consistency.js +66 -0
- package/dist/contract-resources.js +62 -0
- package/dist/coverage-consistency.js +62 -0
- package/dist/coverage-registry.js +76 -0
- package/dist/coverage.js +130 -0
- package/dist/data-list-filter.js +114 -0
- package/dist/discovery.js +390 -0
- package/dist/endpoints.js +154 -0
- package/dist/environment-policy.js +26 -0
- package/dist/execution-metadata.js +1092 -0
- package/dist/fake/app.js +45 -0
- package/dist/fake/b2-registration.js +594 -0
- package/dist/fake/businessrule.js +242 -0
- package/dist/fake/datarule.js +72 -0
- package/dist/fake/dictionary.js +108 -0
- package/dist/fake/environment.js +30 -0
- package/dist/fake/field.js +167 -0
- package/dist/fake/form.js +101 -0
- package/dist/fake/index.js +121 -0
- package/dist/fake/list-view.js +289 -0
- package/dist/fake/model.js +136 -0
- package/dist/fake/online-js.js +18 -0
- package/dist/fake/report.js +253 -0
- package/dist/fake/routes.js +47 -0
- package/dist/fake/rule-lifecycle.js +37 -0
- package/dist/fake/runtime-data.js +367 -0
- package/dist/fake/state.js +67 -0
- package/dist/fake/workflow.js +364 -0
- package/dist/field-change.js +200 -0
- package/dist/field-families.js +896 -0
- package/dist/form-layout.js +111 -0
- package/dist/form-support.js +821 -0
- package/dist/goal-routes.js +468 -0
- package/dist/governed-execution.js +87 -0
- package/dist/human-summary.js +212 -0
- package/dist/identity.js +62 -0
- package/dist/intent/baseline.js +57 -0
- package/dist/intent/capabilities/bizservice.js +274 -0
- package/dist/intent/capabilities/businessrule.js +493 -0
- package/dist/intent/capabilities/datarule.js +187 -0
- package/dist/intent/capabilities/field.js +545 -0
- package/dist/intent/capabilities/form.js +136 -0
- package/dist/intent/capabilities/index.js +64 -0
- package/dist/intent/capabilities/listview.js +157 -0
- package/dist/intent/capabilities/model.js +100 -0
- package/dist/intent/capabilities/onlinejs.js +108 -0
- package/dist/intent/capabilities/report.js +355 -0
- package/dist/intent/capabilities/workflow.js +458 -0
- package/dist/intent/capability.js +6 -0
- package/dist/intent/cli.js +91 -0
- package/dist/intent/compare.js +56 -0
- package/dist/intent/compiler.js +79 -0
- package/dist/intent/dsl.js +129 -0
- package/dist/intent/plan-file.js +63 -0
- package/dist/intent/readback.js +65 -0
- package/dist/intent/schema.js +158 -0
- package/dist/intent/validation.js +30 -0
- package/dist/intent/yaml.js +315 -0
- package/dist/json-column.js +68 -0
- package/dist/lanes/app-contract.js +95 -0
- package/dist/lanes/app-coverage.js +16 -0
- package/dist/lanes/app.js +174 -0
- package/dist/lanes/apply-changes.js +231 -0
- package/dist/lanes/b2-registration-contract.js +292 -0
- package/dist/lanes/b2-registration-coverage.js +48 -0
- package/dist/lanes/b2-registration.js +1187 -0
- package/dist/lanes/businessrule-contract.js +232 -0
- package/dist/lanes/businessrule-coverage.js +16 -0
- package/dist/lanes/businessrule.js +221 -0
- package/dist/lanes/contract-support.js +65 -0
- package/dist/lanes/coverage-declaration.js +9 -0
- package/dist/lanes/datarule-contract.js +155 -0
- package/dist/lanes/datarule-coverage.js +19 -0
- package/dist/lanes/datarule-protocol.js +308 -0
- package/dist/lanes/datarule.js +818 -0
- package/dist/lanes/dictionary-contract.js +78 -0
- package/dist/lanes/dictionary-coverage.js +24 -0
- package/dist/lanes/dictionary.js +235 -0
- package/dist/lanes/environment-contract.js +70 -0
- package/dist/lanes/environment-coverage.js +14 -0
- package/dist/lanes/environment.js +163 -0
- package/dist/lanes/field-contract.js +223 -0
- package/dist/lanes/field-coverage.js +27 -0
- package/dist/lanes/field.js +374 -0
- package/dist/lanes/form-contract.js +97 -0
- package/dist/lanes/form-coverage.js +16 -0
- package/dist/lanes/form.js +185 -0
- package/dist/lanes/lane-ids.js +34 -0
- package/dist/lanes/list-view-contract.js +175 -0
- package/dist/lanes/list-view-coverage.js +20 -0
- package/dist/lanes/list-view-shapes.js +1207 -0
- package/dist/lanes/list-view.js +578 -0
- package/dist/lanes/meta-contract.js +85 -0
- package/dist/lanes/meta.js +255 -0
- package/dist/lanes/model-contract.js +168 -0
- package/dist/lanes/model-coverage.js +20 -0
- package/dist/lanes/model.js +986 -0
- package/dist/lanes/online-js-contract.js +99 -0
- package/dist/lanes/online-js-coverage.js +28 -0
- package/dist/lanes/online-js.js +127 -0
- package/dist/lanes/report-contract.js +144 -0
- package/dist/lanes/report-coverage.js +21 -0
- package/dist/lanes/report.js +476 -0
- package/dist/lanes/rule-graph.js +1846 -0
- package/dist/lanes/rule-lifecycle-contract.js +92 -0
- package/dist/lanes/rule-lifecycle-coverage.js +20 -0
- package/dist/lanes/rule-lifecycle.js +176 -0
- package/dist/lanes/runtime-data-contract.js +264 -0
- package/dist/lanes/runtime-data-coverage.js +25 -0
- package/dist/lanes/runtime-data.js +1054 -0
- package/dist/lanes/workflow-contract.js +223 -0
- package/dist/lanes/workflow-coverage.js +40 -0
- package/dist/lanes/workflow.js +1813 -0
- package/dist/online-js-layout.js +58 -0
- package/dist/online-js-source.js +276 -0
- package/dist/package-tool.js +51 -0
- package/dist/package.js +73 -0
- package/dist/plan.js +73 -0
- package/dist/read.js +144 -0
- package/dist/redact.js +28 -0
- package/dist/rule-support.js +587 -0
- package/dist/runtime-support.js +134 -0
- package/dist/server.js +92 -0
- package/dist/session-manager.js +30 -0
- package/dist/session.js +149 -0
- package/dist/skills.js +112 -0
- package/dist/support.js +98 -0
- package/dist/tool-types.js +127 -0
- package/dist/tools.js +59 -0
- package/dist/usage-log.js +197 -0
- package/dist/wire.js +287 -0
- package/dist/workflow-support.js +99 -0
- package/dist/write-lease.js +26 -0
- package/dist/write-lock.js +109 -0
- package/dist/write.js +176 -0
- package/dist/zip.js +156 -0
- package/docs/tool-surface-map.md +42 -0
- package/package.json +71 -6
- package/skills/omc-acceptance-criteria.md +47 -0
- package/skills/omc-business-configuration.md +257 -0
- package/skills/omc-capabilities.md +197 -0
- package/skills/omc-capability-scouting.md +55 -0
- package/skills/omc-config-draft-review.md +171 -0
- package/skills/omc-five-piece-flow.md +35 -0
- package/skills/omc-glossary.md +86 -0
- package/skills/omc-refusals.md +110 -0
- package/skills/omc-requirement-analysis.md +161 -0
- package/skills/omc-requirement-vocabulary.md +51 -0
- package/skills/omc-start-here.md +82 -0
- package/skills/omc-tool-selection.md +119 -0
- package/skills/omc-write-hazards.md +87 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CFG-naming rules (borrowed from cpdocs base3.0 `cp-bizmodel/model-code.js`
|
|
3
|
+
* and `cp-bizproperty/field-code.js`): model codes are at most 26 chars, field
|
|
4
|
+
* codes at most 28, both must be a single lowerCamel or lower_snake token, and
|
|
5
|
+
* platform system/reserved codes are refused. These are local dry-lint checks —
|
|
6
|
+
* a violation would otherwise only surface on the environment (or worse,
|
|
7
|
+
* silently poison the model, as the pre-fix tools did).
|
|
8
|
+
*/
|
|
9
|
+
export const MODEL_CODE_MAX_LENGTH = 26;
|
|
10
|
+
export const FIELD_CODE_MAX_LENGTH = 28;
|
|
11
|
+
const CODE_PATTERNS = {
|
|
12
|
+
lowerCamel: /^[a-z][A-Za-z0-9]*$/u,
|
|
13
|
+
lower_snake: /^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/u,
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* 8.6.26 system / audit column codes a business-authored model or field must
|
|
17
|
+
* never claim (they are minted by the platform; reusing one collides with the
|
|
18
|
+
* engine's own columns). Mirrors base3.0's reserved sets.
|
|
19
|
+
*/
|
|
20
|
+
export const RESERVED_CODES = new Set([
|
|
21
|
+
"id",
|
|
22
|
+
"name",
|
|
23
|
+
"creater",
|
|
24
|
+
"createdDeptId",
|
|
25
|
+
"owner",
|
|
26
|
+
"ownerDeptId",
|
|
27
|
+
"createdTime",
|
|
28
|
+
"modifier",
|
|
29
|
+
"modifiedTime",
|
|
30
|
+
"workflowInstanceId",
|
|
31
|
+
"sequenceNo",
|
|
32
|
+
"sequenceStatus",
|
|
33
|
+
"ownerDeptQueryCode",
|
|
34
|
+
"version",
|
|
35
|
+
].map((code) => code.toLowerCase()));
|
|
36
|
+
/** Infer the style a code is written in (underscores → snake, else camel). */
|
|
37
|
+
export function inferCodeStyle(code) {
|
|
38
|
+
if (code.includes("_"))
|
|
39
|
+
return CODE_PATTERNS.lower_snake.test(code) ? "lower_snake" : null;
|
|
40
|
+
return CODE_PATTERNS.lowerCamel.test(code) ? "lowerCamel" : null;
|
|
41
|
+
}
|
|
42
|
+
/** Returns the (normalized) violations; an empty array means the code is valid. */
|
|
43
|
+
export function validateBusinessCode(rawCode, options) {
|
|
44
|
+
const maxLength = options.kind === "model" ? MODEL_CODE_MAX_LENGTH : FIELD_CODE_MAX_LENGTH;
|
|
45
|
+
const code = typeof rawCode === "string" ? rawCode.trim() : "";
|
|
46
|
+
const violations = [];
|
|
47
|
+
if (!code) {
|
|
48
|
+
violations.push({ code, classification: "code_empty", message: "code 不能为空" });
|
|
49
|
+
return violations;
|
|
50
|
+
}
|
|
51
|
+
if (code.length > maxLength)
|
|
52
|
+
violations.push({ code, classification: "code_too_long", message: `code "${code}" 长 ${code.length} 字符,超过 ${options.kind === "model" ? "模型" : "字段"}上限 ${maxLength}(平台约束)` });
|
|
53
|
+
const detected = inferCodeStyle(code);
|
|
54
|
+
if (options.style) {
|
|
55
|
+
if (!CODE_PATTERNS[options.style].test(code))
|
|
56
|
+
violations.push({ code, classification: "code_style", message: `code "${code}" 不符合 ${options.style}(期望 ${options.style === "lowerCamel" ? "小驼峰 aBc" : "小写下划线 a_bc"})` });
|
|
57
|
+
}
|
|
58
|
+
else if (!detected) {
|
|
59
|
+
violations.push({ code, classification: "code_style", message: `code "${code}" 既不是 lowerCamel 也不是 lower_snake(须以字母开头,仅含字母/数字/下划线,且不得连续或结尾下划线)` });
|
|
60
|
+
}
|
|
61
|
+
if (RESERVED_CODES.has(code.toLowerCase()))
|
|
62
|
+
violations.push({ code, classification: "code_reserved", message: `code "${code}" 是平台系统/保留字段编码,业务模型与字段不得使用(会与平台自身列冲突)` });
|
|
63
|
+
return violations;
|
|
64
|
+
}
|
|
65
|
+
/** Convenience for the dry-lint call sites: true when validateBusinessCode has no violations. */
|
|
66
|
+
export function businessCodeValid(rawCode, options) {
|
|
67
|
+
return validateBusinessCode(rawCode, options).length === 0;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=code-rules.js.map
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { homedir, tmpdir } from "node:os";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { CLI_DEFAULT_OUTPUT_BYTES, CLI_MAX_OUTPUT_BYTES, createArtifact, isWithinRoot, readArtifact } from "./artifacts.js";
|
|
6
|
+
import { CLI_EXIT_CODES, classifyResult, envelope, errorEnvelope } from "./cli-contract.js";
|
|
7
|
+
import { writeEnvelope, outputOptions } from "./cli-output.js";
|
|
8
|
+
const UNCERTAIN_NEXT_ACTION = "read back the requestId before retrying; retry only after the outcome is established";
|
|
9
|
+
/**
|
|
10
|
+
* Run a CLI action inside the transport band: validate the budget up front (a
|
|
11
|
+
* bad `--max-output-bytes` becomes the shared invalid-input envelope, exactly
|
|
12
|
+
* once), then hand the handler its transport.
|
|
13
|
+
*/
|
|
14
|
+
export async function withTransport(options, request, run) {
|
|
15
|
+
let budget;
|
|
16
|
+
try {
|
|
17
|
+
budget = options.maxOutputBytes === undefined ? CLI_DEFAULT_OUTPUT_BYTES : checkedOutputBudget(Number(options.maxOutputBytes));
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
const parsed = errorEnvelope(error, "invalid-input", CLI_EXIT_CODES.invalidInput, request);
|
|
21
|
+
writeEnvelope(parsed.envelope);
|
|
22
|
+
return parsed.exitCode;
|
|
23
|
+
}
|
|
24
|
+
return await run({ request, maxOutputBytes: budget });
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Write the single result envelope and return the exit code. An `uncertain`
|
|
28
|
+
* result always carries a recovery `nextAction` (the documented contract); a
|
|
29
|
+
* result that already states a structured one keeps it.
|
|
30
|
+
*/
|
|
31
|
+
export function emitTransportResult(result, transport) {
|
|
32
|
+
const classified = classifyResult(result);
|
|
33
|
+
const withRecovery = classified.status === "uncertain" && result.nextAction === undefined ? { ...result, nextAction: UNCERTAIN_NEXT_ACTION } : result;
|
|
34
|
+
writeEnvelope(envelope(classified.status, boundResult(withRecovery, transport.maxOutputBytes), transport.request, classified.error));
|
|
35
|
+
return classified.exitCode;
|
|
36
|
+
}
|
|
37
|
+
/** Write an invalid-input envelope (exit 10) — the shape every pre-wire refusal uses. */
|
|
38
|
+
export const emitResult = emitTransportResult;
|
|
39
|
+
export function emitInvalid(transport, options) {
|
|
40
|
+
const result = options.blocker === undefined ? {} : { status: "error", blocker: options.blocker };
|
|
41
|
+
const error = { code: options.code ?? "invalid-input", message: options.message, retryable: false };
|
|
42
|
+
writeEnvelope(envelope("error", result, transport.request, error));
|
|
43
|
+
return CLI_EXIT_CODES.invalidInput;
|
|
44
|
+
}
|
|
45
|
+
/** Map a thrown error to the shared invalid-input envelope. */
|
|
46
|
+
export function emitThrown(error, transport) {
|
|
47
|
+
const parsed = errorEnvelope(error, "invalid-input", CLI_EXIT_CODES.invalidInput, transport.request);
|
|
48
|
+
writeEnvelope(parsed.envelope);
|
|
49
|
+
return parsed.exitCode;
|
|
50
|
+
}
|
|
51
|
+
export function outputBudget(args) {
|
|
52
|
+
const index = args.indexOf("--max-output-bytes");
|
|
53
|
+
if (index < 0)
|
|
54
|
+
return CLI_DEFAULT_OUTPUT_BYTES;
|
|
55
|
+
return checkedOutputBudget(Number(args[index + 1]));
|
|
56
|
+
}
|
|
57
|
+
export function checkedOutputBudget(value) {
|
|
58
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > CLI_MAX_OUTPUT_BYTES)
|
|
59
|
+
throw new Error(`--max-output-bytes must be an integer from 1 to ${CLI_MAX_OUTPUT_BYTES}`);
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
export function artifactRoot() {
|
|
63
|
+
return resolve(process.env.OMC_ARTIFACTS_HOME ?? joinConfigRoot(), "artifacts");
|
|
64
|
+
}
|
|
65
|
+
export function joinConfigRoot() {
|
|
66
|
+
return resolve(process.env.OMC_CONFIG_HOME ?? process.cwd());
|
|
67
|
+
}
|
|
68
|
+
/** Bound a result to the stdout budget; over budget, spill it to an artifact reference. */
|
|
69
|
+
export function boundResult(result, budget) {
|
|
70
|
+
const sanitized = sanitizePaths(result);
|
|
71
|
+
const serialized = JSON.stringify(sanitized);
|
|
72
|
+
if (outputOptions().outFile)
|
|
73
|
+
return sanitized;
|
|
74
|
+
if (Buffer.byteLength(serialized) <= budget)
|
|
75
|
+
return sanitized;
|
|
76
|
+
const artifact = createArtifact(artifactRoot(), JSON.stringify(sanitized, null, 2) + "\n");
|
|
77
|
+
return { status: sanitized.status ?? "ok", truncated: true, artifactId: artifact.artifactId, sha256: artifact.sha256, bytes: Buffer.byteLength(serialized), fetch: artifactFetchHint(artifact.artifactId) };
|
|
78
|
+
}
|
|
79
|
+
/** The retrieval command an agent should run for a spilled/compacted readback. */
|
|
80
|
+
export function artifactFetchHint(artifactId) {
|
|
81
|
+
// Summary-first: the complex benchmark showed agents pulling 43–75 KB bodies
|
|
82
|
+
// raw. `--summary` shows the key/size map so they fetch only what they need.
|
|
83
|
+
// Kept terse on purpose: this line rides EVERY write and every spilled read
|
|
84
|
+
// (2026-09-25 cost pass — the parenthetical prose was 80 B of the 133 B).
|
|
85
|
+
return `omc artifact get ${artifactId} --summary(再 --keys/--jq)`;
|
|
86
|
+
}
|
|
87
|
+
/** Replace local paths with a redaction marker (never leak the host layout). */
|
|
88
|
+
function sanitizePaths(value) {
|
|
89
|
+
if (typeof value === "string")
|
|
90
|
+
return redactLocalPath(value);
|
|
91
|
+
if (Array.isArray(value))
|
|
92
|
+
return value.map(sanitizePaths);
|
|
93
|
+
if (!value || typeof value !== "object")
|
|
94
|
+
return value;
|
|
95
|
+
const output = {};
|
|
96
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
97
|
+
if ((key === "detailFile" || key === "progressFile") && typeof entry === "string") {
|
|
98
|
+
const content = safeToolArtifactContent(entry);
|
|
99
|
+
if (content) {
|
|
100
|
+
const artifact = createArtifact(artifactRoot(), content);
|
|
101
|
+
output[`${key === "detailFile" ? "detail" : "progress"}ArtifactId`] = artifact.artifactId;
|
|
102
|
+
output[`${key === "detailFile" ? "detail" : "progress"}Sha256`] = artifact.sha256;
|
|
103
|
+
output.fetch = artifactFetchHint(artifact.artifactId);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
output[`${key === "detailFile" ? "detail" : "progress"}ArtifactId`] = null;
|
|
107
|
+
}
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
output[key] = sanitizePaths(entry);
|
|
111
|
+
}
|
|
112
|
+
return output;
|
|
113
|
+
}
|
|
114
|
+
function safeToolArtifactContent(path) {
|
|
115
|
+
try {
|
|
116
|
+
const roots = [artifactRoot(), resolve(process.cwd(), ".omc", "artifacts")];
|
|
117
|
+
const resolved = resolve(path);
|
|
118
|
+
if (!roots.some((root) => isWithinRoot(root, resolved)))
|
|
119
|
+
return null;
|
|
120
|
+
if (realpathSync(resolved) !== resolved)
|
|
121
|
+
return null;
|
|
122
|
+
return readFileSync(resolved);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function escapeRegExp(text) {
|
|
129
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Redact local paths WITHIN a string, not the whole string. The old form tested
|
|
133
|
+
* `text.includes(prefix)` and replaced the entire value with the marker, so any
|
|
134
|
+
* text that merely MENTIONED a local path — e.g. a methodology section quoting
|
|
135
|
+
* `node .cpdocs/omc/cli.js` — came back as a bare `[local-path-redacted]` and its
|
|
136
|
+
* content was lost (bench40: `help omc-acceptance-criteria --section Per-write`
|
|
137
|
+
* returned nothing but the marker). Now only the matched path substrings are
|
|
138
|
+
* replaced, so the surrounding prose survives.
|
|
139
|
+
*/
|
|
140
|
+
function redactLocalPath(text) {
|
|
141
|
+
if (!/[\\/]/.test(text))
|
|
142
|
+
return text;
|
|
143
|
+
const roots = [joinConfigRoot(), process.cwd(), homedir(), tmpdir()]
|
|
144
|
+
.filter((root) => typeof root === "string" && root.length > 1)
|
|
145
|
+
.map((root) => root.replace(/[\\/]+$/, ""));
|
|
146
|
+
let output = text;
|
|
147
|
+
for (const root of roots) {
|
|
148
|
+
output = output.replace(new RegExp(`${escapeRegExp(root)}[\\/][^\\s"'\`)\\]}]*`, "g"), "[local-path-redacted]");
|
|
149
|
+
}
|
|
150
|
+
// A relative `.omc/...` artifact path is host-layout revealing too.
|
|
151
|
+
output = output.replace(/(?<![\w.-])\.omc[\\/][^\s"'\`)\]}]*/g, "[local-path-redacted]");
|
|
152
|
+
return output;
|
|
153
|
+
}
|
|
154
|
+
export { readArtifact };
|
|
155
|
+
//# sourceMappingURL=command-transport.js.map
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { existsSync, readFileSync, copyFileSync, chmodSync, mkdirSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { sync as writeFileAtomicSync } from "write-file-atomic";
|
|
5
|
+
/**
|
|
6
|
+
* Removed governance keys. This is a development branch: seeing one of these in
|
|
7
|
+
* an operator's omc.config.json is an error to be fixed, never silently ignored
|
|
8
|
+
* (silently ignoring `selfApprove` would leave a false sense that it still
|
|
9
|
+
* protects anything).
|
|
10
|
+
*/
|
|
11
|
+
export const REMOVED_PROFILE_KEYS = ["selfApprove", "productionWriteGrant", "mcpSelfApprove"];
|
|
12
|
+
export function removedProfileKeys(profile) {
|
|
13
|
+
const raw = profile;
|
|
14
|
+
return REMOVED_PROFILE_KEYS.filter((key) => raw[key] !== undefined);
|
|
15
|
+
}
|
|
16
|
+
export class ConfigurationError extends Error {
|
|
17
|
+
}
|
|
18
|
+
export function defaultPaths(projectDir = process.cwd()) {
|
|
19
|
+
const configRoot = process.env.OMC_CONFIG_HOME
|
|
20
|
+
? resolve(process.env.OMC_CONFIG_HOME)
|
|
21
|
+
: join(homedir(), ".config", "omc");
|
|
22
|
+
return {
|
|
23
|
+
configPath: join(configRoot, "omc.config.json"),
|
|
24
|
+
credentialsPath: join(configRoot, "credentials"),
|
|
25
|
+
projectEnvPath: join(resolve(projectDir), ".omc-env"),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export class ConfigStore {
|
|
29
|
+
paths;
|
|
30
|
+
constructor(paths = defaultPaths()) {
|
|
31
|
+
this.paths = paths;
|
|
32
|
+
}
|
|
33
|
+
atomicWrite(path, text, mode) {
|
|
34
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
35
|
+
if (existsSync(path)) {
|
|
36
|
+
try {
|
|
37
|
+
const previousMode = statSync(path).mode & 0o777;
|
|
38
|
+
copyFileSync(path, `${path}.bak`);
|
|
39
|
+
chmodSync(`${path}.bak`, previousMode);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// best-effort backup: the atomic rename below is the durability guarantee
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
writeFileAtomicSync(path, text, { mode });
|
|
46
|
+
}
|
|
47
|
+
readConfig() {
|
|
48
|
+
if (!existsSync(this.paths.configPath))
|
|
49
|
+
return { environments: {} };
|
|
50
|
+
let parsed;
|
|
51
|
+
let text;
|
|
52
|
+
try {
|
|
53
|
+
text = readFileSync(this.paths.configPath, "utf8");
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
throw new ConfigurationError(`omc.config.json is unreadable: ${error.message}`);
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
parsed = JSON.parse(text);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (existsSync(`${this.paths.configPath}.bak`)) {
|
|
63
|
+
try {
|
|
64
|
+
parsed = JSON.parse(readFileSync(`${this.paths.configPath}.bak`, "utf8"));
|
|
65
|
+
console.error(`omc.config.json 损坏,已从 .bak 备份恢复(上次已知良好状态): ${error.message}`);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
throw new ConfigurationError(`omc.config.json is not valid JSON: ${error.message}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else
|
|
72
|
+
throw new ConfigurationError(`omc.config.json is not valid JSON: ${error.message}`);
|
|
73
|
+
}
|
|
74
|
+
if (!parsed || typeof parsed !== "object" || !parsed || typeof parsed.environments !== "object")
|
|
75
|
+
throw new ConfigurationError("omc.config.json must contain an environments object");
|
|
76
|
+
const config = parsed;
|
|
77
|
+
const normalized = {};
|
|
78
|
+
for (const [name, profile] of Object.entries(config.environments)) {
|
|
79
|
+
if (!profile || typeof profile.baseUrl !== "string")
|
|
80
|
+
throw new ConfigurationError(`environment ${name} must declare baseUrl`);
|
|
81
|
+
// applicationCode is an optional default; an absent value is normalized
|
|
82
|
+
// to "" (the agent discovers applications at runtime).
|
|
83
|
+
if (profile.applicationCode !== undefined && typeof profile.applicationCode !== "string")
|
|
84
|
+
throw new ConfigurationError(`environment ${name} applicationCode must be a string when present`);
|
|
85
|
+
if (profile.engineCode !== undefined && typeof profile.engineCode !== "string")
|
|
86
|
+
throw new ConfigurationError(`environment ${name} engineCode must be a string when present`);
|
|
87
|
+
for (const key of ["isDefault", "production", "writable"])
|
|
88
|
+
if (profile[key] !== undefined && typeof profile[key] !== "boolean")
|
|
89
|
+
throw new ConfigurationError(`environment ${name} ${key} must be boolean when present`);
|
|
90
|
+
const removed = removedProfileKeys(profile);
|
|
91
|
+
if (removed.length > 0)
|
|
92
|
+
throw new ConfigurationError(`environment ${name} carries removed governance keys: ${removed.join(", ")}. Writing is decided by "writable" alone, and a production write needs a per-write confirmProduction — delete these keys.`);
|
|
93
|
+
const engineCode = profile.engineCode?.trim();
|
|
94
|
+
normalized[name] = { ...profile, applicationCode: profile.applicationCode ?? "", ...(engineCode ? { engineCode } : {}), writable: profile.writable === true };
|
|
95
|
+
}
|
|
96
|
+
return { environments: normalized };
|
|
97
|
+
}
|
|
98
|
+
writeConfig(config) {
|
|
99
|
+
this.atomicWrite(this.paths.configPath, JSON.stringify(config, null, 2) + "\n", 0o644);
|
|
100
|
+
}
|
|
101
|
+
upsertEnvironment(name, profile) {
|
|
102
|
+
if (!name.trim())
|
|
103
|
+
throw new ConfigurationError("environment name is required");
|
|
104
|
+
const config = this.readConfig();
|
|
105
|
+
const environments = { ...config.environments, [name]: profile };
|
|
106
|
+
if (profile.isDefault)
|
|
107
|
+
for (const [other, otherProfile] of Object.entries(environments))
|
|
108
|
+
if (other !== name)
|
|
109
|
+
environments[other] = { ...otherProfile, isDefault: false };
|
|
110
|
+
const next = { environments };
|
|
111
|
+
this.writeConfig(next);
|
|
112
|
+
return next;
|
|
113
|
+
}
|
|
114
|
+
removeEnvironment(name) {
|
|
115
|
+
const config = this.readConfig();
|
|
116
|
+
const environments = { ...config.environments };
|
|
117
|
+
delete environments[name];
|
|
118
|
+
this.writeConfig({ environments });
|
|
119
|
+
return config;
|
|
120
|
+
}
|
|
121
|
+
listEnvironments() {
|
|
122
|
+
return Object.entries(this.readConfig().environments).map(([name, profile]) => ({ name, profile }));
|
|
123
|
+
}
|
|
124
|
+
readCredential(name) {
|
|
125
|
+
const inline = process.env.OMC_CLOUDPIVOT_CREDENTIAL;
|
|
126
|
+
if (inline) {
|
|
127
|
+
try {
|
|
128
|
+
const parsed = JSON.parse(inline);
|
|
129
|
+
if (typeof parsed.username === "string" && typeof parsed.password === "string")
|
|
130
|
+
return parsed;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
throw new ConfigurationError("OMC_CLOUDPIVOT_CREDENTIAL must be JSON {username,password}");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (!existsSync(this.paths.credentialsPath))
|
|
137
|
+
throw new ConfigurationError(`no credentials for environment ${name}; run omc setup, or set OMC_CLOUDPIVOT_CREDENTIAL`);
|
|
138
|
+
const parsed = JSON.parse(readFileSync(this.paths.credentialsPath, "utf8"));
|
|
139
|
+
const credential = parsed[name];
|
|
140
|
+
if (!credential || typeof credential.username !== "string" || typeof credential.password !== "string")
|
|
141
|
+
throw new ConfigurationError(`credentials store has no entry for environment ${name}`);
|
|
142
|
+
return credential;
|
|
143
|
+
}
|
|
144
|
+
writeCredential(name, credential) {
|
|
145
|
+
mkdirSync(dirname(this.paths.credentialsPath), { recursive: true });
|
|
146
|
+
const existing = existsSync(this.paths.credentialsPath)
|
|
147
|
+
? JSON.parse(readFileSync(this.paths.credentialsPath, "utf8"))
|
|
148
|
+
: {};
|
|
149
|
+
existing[name] = credential;
|
|
150
|
+
this.atomicWrite(this.paths.credentialsPath, JSON.stringify(existing, null, 2) + "\n", 0o600);
|
|
151
|
+
chmodSync(this.paths.credentialsPath, 0o600);
|
|
152
|
+
}
|
|
153
|
+
credentialsMode() {
|
|
154
|
+
return existsSync(this.paths.credentialsPath) ? statSync(this.paths.credentialsPath).mode & 0o777 : undefined;
|
|
155
|
+
}
|
|
156
|
+
/** Available environment names, for error messages — stops agents guessing or spelunking ~/.config. */
|
|
157
|
+
availableEnvironmentNames() {
|
|
158
|
+
return Object.keys(this.readConfig().environments).join("、") || "(none configured; run omc setup)";
|
|
159
|
+
}
|
|
160
|
+
/** Resolve the active environment for a call. */
|
|
161
|
+
resolveEnvironment(explicit) {
|
|
162
|
+
if (explicit && explicit.trim()) {
|
|
163
|
+
const profile = this.readConfig().environments[explicit];
|
|
164
|
+
if (!profile)
|
|
165
|
+
throw new ConfigurationError(`unknown environment "${explicit}"; available: ${this.availableEnvironmentNames()} (doctor prints the bound 环境=; do not guess or read ~/.config/omc manually)`);
|
|
166
|
+
return { name: explicit, profile, source: "explicit" };
|
|
167
|
+
}
|
|
168
|
+
if (process.env.OMC_ENV) {
|
|
169
|
+
const profile = this.readConfig().environments[process.env.OMC_ENV];
|
|
170
|
+
if (!profile)
|
|
171
|
+
throw new ConfigurationError(`OMC_ENV=${process.env.OMC_ENV} is not configured; available: ${this.availableEnvironmentNames()}`);
|
|
172
|
+
return { name: process.env.OMC_ENV, profile, source: "OMC_ENV" };
|
|
173
|
+
}
|
|
174
|
+
if (existsSync(this.paths.projectEnvPath)) {
|
|
175
|
+
const name = readFileSync(this.paths.projectEnvPath, "utf8").trim();
|
|
176
|
+
if (name) {
|
|
177
|
+
const profile = this.readConfig().environments[name];
|
|
178
|
+
if (!profile)
|
|
179
|
+
throw new ConfigurationError(`project .omc-env binds "${name}" which is not configured in omc.config.json; available: ${this.availableEnvironmentNames()} (run omc setup)`);
|
|
180
|
+
return { name, profile, source: ".omc-env" };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const entry = this.listEnvironments().find((item) => item.profile.isDefault);
|
|
184
|
+
if (!entry)
|
|
185
|
+
throw new ConfigurationError("no default environment; run omc setup or pass environment explicitly");
|
|
186
|
+
return { name: entry.name, profile: entry.profile, source: "default" };
|
|
187
|
+
}
|
|
188
|
+
bindProjectEnvironment(name) {
|
|
189
|
+
const config = this.readConfig();
|
|
190
|
+
if (!config.environments[name])
|
|
191
|
+
throw new ConfigurationError(`unknown environment "${name}"; available: ${this.availableEnvironmentNames()}`);
|
|
192
|
+
this.atomicWrite(this.paths.projectEnvPath, name + "\n", 0o644);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=config-store.js.map
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { WriteLeaseQueue } from "./write-lease.js";
|
|
3
|
+
import { loadCoverageManifest } from "./coverage.js";
|
|
4
|
+
/**
|
|
5
|
+
* Tool-context assembly: session manager + write lease + coverage manifest +
|
|
6
|
+
* artifact/cache stores. The one place the governed pipelines' collaborators
|
|
7
|
+
* are wired together.
|
|
8
|
+
*/
|
|
9
|
+
export function makeToolContext(input) {
|
|
10
|
+
return {
|
|
11
|
+
sessions: input.sessions,
|
|
12
|
+
lease: new WriteLeaseQueue(),
|
|
13
|
+
manifest: input.manifest ?? loadCoverageManifest(),
|
|
14
|
+
artifactsDir: input.artifactsDir ?? join(process.cwd(), ".omc", "artifacts"),
|
|
15
|
+
readbackCache: new Map(),
|
|
16
|
+
readbackCacheBypass: 0,
|
|
17
|
+
schemaCache: new Set(),
|
|
18
|
+
secretsOf: () => [],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { LANE_CONTRACTS } from "./contract-resources.js";
|
|
5
|
+
export function assertContractBindings(contracts = LANE_CONTRACTS, bindings = {}) {
|
|
6
|
+
const contractLanes = new Set(contracts.map((contract) => contract.lane));
|
|
7
|
+
for (const lane of Object.keys(bindings)) {
|
|
8
|
+
if (!contractLanes.has(lane))
|
|
9
|
+
throw new Error(`contract binding lane ${lane} has no contract`);
|
|
10
|
+
}
|
|
11
|
+
for (const contract of contracts) {
|
|
12
|
+
const laneBindings = { ...(bindings[contract.lane] ?? {}), ...(contract.wireBindings ?? {}) };
|
|
13
|
+
const bindingKeys = new Set(Object.keys(laneBindings));
|
|
14
|
+
for (const key of Object.keys(contract.wire)) {
|
|
15
|
+
const id = `${contract.lane}.${key}`;
|
|
16
|
+
const factBindings = laneBindings[key];
|
|
17
|
+
if (factBindings === undefined)
|
|
18
|
+
throw new Error(`contract fact ${id} has no binding`);
|
|
19
|
+
if (factBindings.length === 0)
|
|
20
|
+
throw new Error(`contract fact ${id} has an empty binding list`);
|
|
21
|
+
for (const binding of factBindings)
|
|
22
|
+
assertBinding(id, binding);
|
|
23
|
+
bindingKeys.delete(key);
|
|
24
|
+
}
|
|
25
|
+
for (const key of bindingKeys)
|
|
26
|
+
throw new Error(`contract binding ${contract.lane}.${key} has no wire fact`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function assertBinding(id, binding) {
|
|
30
|
+
const kinds = ["emitter-constant", "behavior-test", "oracle"];
|
|
31
|
+
if (!kinds.includes(binding.kind))
|
|
32
|
+
throw new Error(`contract fact ${id} has unknown binding kind ${String(binding.kind)}`);
|
|
33
|
+
if (!binding.ref.trim())
|
|
34
|
+
throw new Error(`contract fact ${id} has an empty binding ref`);
|
|
35
|
+
try {
|
|
36
|
+
assertBindingReference(binding.ref);
|
|
37
|
+
binding.verify();
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
throw new Error(`contract fact ${id} binding ${binding.ref} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function assertBindingReference(ref) {
|
|
44
|
+
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
45
|
+
const [target, anchor] = ref.split("#", 2);
|
|
46
|
+
if (target.startsWith("test/")) {
|
|
47
|
+
const filePath = join(packageRoot, target);
|
|
48
|
+
if (!existsSync(filePath))
|
|
49
|
+
throw new Error(`binding file ${target} does not exist`);
|
|
50
|
+
if (anchor && !readFileSync(filePath, "utf8").includes(anchor))
|
|
51
|
+
throw new Error(`binding test anchor ${anchor} does not exist in ${target}`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const separator = target.lastIndexOf(".");
|
|
55
|
+
if (separator < 1 || separator === target.length - 1)
|
|
56
|
+
throw new Error(`binding reference ${ref} is not a module symbol reference`);
|
|
57
|
+
const moduleName = target.slice(0, separator);
|
|
58
|
+
const symbol = target.slice(separator + 1);
|
|
59
|
+
const filePath = join(packageRoot, "src", `${moduleName}.ts`);
|
|
60
|
+
if (!existsSync(filePath))
|
|
61
|
+
throw new Error(`binding module ${moduleName} does not exist`);
|
|
62
|
+
const source = readFileSync(filePath, "utf8");
|
|
63
|
+
if (!new RegExp(`export\\s+(?:async\\s+)?(?:const|function|class)\\s+${symbol}\\b`).test(source))
|
|
64
|
+
throw new Error(`binding symbol ${symbol} does not exist in ${moduleName}`);
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=contract-consistency.js.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { APP_LANE_CONTRACT } from "./lanes/app-contract.js";
|
|
2
|
+
import { B2_LANE_CONTRACT } from "./lanes/b2-registration-contract.js";
|
|
3
|
+
import { BUSINESSRULE_LANE_CONTRACT } from "./lanes/businessrule-contract.js";
|
|
4
|
+
import { DATARULE_LANE_CONTRACT } from "./lanes/datarule-contract.js";
|
|
5
|
+
import { DICTIONARY_LANE_CONTRACT } from "./lanes/dictionary-contract.js";
|
|
6
|
+
import { ENVIRONMENT_LANE_CONTRACT } from "./lanes/environment-contract.js";
|
|
7
|
+
import { FIELD_LANE_CONTRACT } from "./lanes/field-contract.js";
|
|
8
|
+
import { FORM_LANE_CONTRACT } from "./lanes/form-contract.js";
|
|
9
|
+
import { LIST_VIEW_LANE_CONTRACT } from "./lanes/list-view-contract.js";
|
|
10
|
+
import { META_LANE_CONTRACT } from "./lanes/meta-contract.js";
|
|
11
|
+
import { MODEL_LANE_CONTRACT } from "./lanes/model-contract.js";
|
|
12
|
+
import { ONLINE_JS_LANE_CONTRACT } from "./lanes/online-js-contract.js";
|
|
13
|
+
import { RULE_LIFECYCLE_LANE_CONTRACT } from "./lanes/rule-lifecycle-contract.js";
|
|
14
|
+
import { REPORT_LANE_CONTRACT } from "./lanes/report-contract.js";
|
|
15
|
+
import { RUNTIME_DATA_LANE_CONTRACT } from "./lanes/runtime-data-contract.js";
|
|
16
|
+
import { WORKFLOW_LANE_CONTRACT } from "./lanes/workflow-contract.js";
|
|
17
|
+
export const LANE_CONTRACTS = Object.freeze([
|
|
18
|
+
APP_LANE_CONTRACT,
|
|
19
|
+
B2_LANE_CONTRACT,
|
|
20
|
+
BUSINESSRULE_LANE_CONTRACT,
|
|
21
|
+
DATARULE_LANE_CONTRACT,
|
|
22
|
+
DICTIONARY_LANE_CONTRACT,
|
|
23
|
+
ENVIRONMENT_LANE_CONTRACT,
|
|
24
|
+
FIELD_LANE_CONTRACT,
|
|
25
|
+
FORM_LANE_CONTRACT,
|
|
26
|
+
LIST_VIEW_LANE_CONTRACT,
|
|
27
|
+
META_LANE_CONTRACT,
|
|
28
|
+
MODEL_LANE_CONTRACT,
|
|
29
|
+
ONLINE_JS_LANE_CONTRACT,
|
|
30
|
+
RULE_LIFECYCLE_LANE_CONTRACT,
|
|
31
|
+
REPORT_LANE_CONTRACT,
|
|
32
|
+
RUNTIME_DATA_LANE_CONTRACT,
|
|
33
|
+
WORKFLOW_LANE_CONTRACT,
|
|
34
|
+
]);
|
|
35
|
+
export function contractMarkdown(contract) {
|
|
36
|
+
const tools = Object.entries(contract.tools).map(([name, tool]) => `- ${name} — ${tool.scope}\n ${tool.description}`).join("\n");
|
|
37
|
+
// Ticket 55: the `evidence` ledger is maintainer-facing (coverage
|
|
38
|
+
// consistency keeps using LaneContractShape.evidence), and it names repo
|
|
39
|
+
// source paths (.ts / .scratch) that an agent would chase instead of
|
|
40
|
+
// trusting this self-contained contract. It is deliberately NOT rendered
|
|
41
|
+
// into the caller-facing markdown.
|
|
42
|
+
return [
|
|
43
|
+
`# ${contract.lane}`,
|
|
44
|
+
`Family: ${contract.family}`,
|
|
45
|
+
`Reference version: ${contract.referenceVersion}`,
|
|
46
|
+
"",
|
|
47
|
+
"## Wire",
|
|
48
|
+
"```json",
|
|
49
|
+
JSON.stringify(contract.wire, null, 2),
|
|
50
|
+
"```",
|
|
51
|
+
"",
|
|
52
|
+
"## Endpoints",
|
|
53
|
+
"```json",
|
|
54
|
+
JSON.stringify(contract.endpoints, null, 2),
|
|
55
|
+
"```",
|
|
56
|
+
"",
|
|
57
|
+
"## Tools",
|
|
58
|
+
tools,
|
|
59
|
+
"",
|
|
60
|
+
].join("\n");
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=contract-resources.js.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { loadCoverageManifest } from "./coverage.js";
|
|
2
|
+
import { assertDeclarationsAlignWithManifest, laneCoverageDeclarations } from "./coverage-registry.js";
|
|
3
|
+
import { assertContractBindings } from "./contract-consistency.js";
|
|
4
|
+
import { isLaneId } from "./lanes/lane-ids.js";
|
|
5
|
+
import { buildToolRegistry } from "./tools.js";
|
|
6
|
+
/**
|
|
7
|
+
* Tool-surface half of the three-way consistency guard: every registered tool
|
|
8
|
+
* that declares a lane/scope must be claimed by exactly one lane declaration
|
|
9
|
+
* with the same lane and scope, and every declared tool must be registered.
|
|
10
|
+
* Accepts the tool list explicitly so a tampered surface can be asserted in
|
|
11
|
+
* tests (the real entry point passes `buildToolRegistry()`).
|
|
12
|
+
*
|
|
13
|
+
* This guard is NOT a duplicate-of-a-duplicate: lane registrations read their
|
|
14
|
+
* scope/description from the lane contract, so it does not reconcile two copies
|
|
15
|
+
* of the metadata. It enforces two things that genuinely can drift — a
|
|
16
|
+
* registered capability tool must carry a coverage claim (or the runtime
|
|
17
|
+
* coverage gate would refuse it), and a coverage declaration must not claim a
|
|
18
|
+
* tool that no longer exists. Keep coverage claims sourcing their scope from
|
|
19
|
+
* the contract (`claim("tool", TOOLS["tool"].scope)`) rather than a local literal;
|
|
20
|
+
* a literal is only held equal by this guard, which is the smell to avoid.
|
|
21
|
+
*/
|
|
22
|
+
export function assertToolSurfaceConsistent(tools) {
|
|
23
|
+
const declaredTools = new Map();
|
|
24
|
+
for (const declaration of laneCoverageDeclarations()) {
|
|
25
|
+
for (const toolClaim of declaration.tools) {
|
|
26
|
+
declaredTools.set(toolClaim.tool, { lane: declaration.lane, scope: toolClaim.scope });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
for (const tool of tools) {
|
|
30
|
+
if (!tool.lane && !tool.scope)
|
|
31
|
+
continue;
|
|
32
|
+
if (!tool.lane || !tool.scope)
|
|
33
|
+
throw new Error(`tool ${tool.name} declares lane/scope incompletely`);
|
|
34
|
+
if (!isLaneId(tool.lane))
|
|
35
|
+
throw new Error(`tool ${tool.name} declares unknown lane ${tool.lane}`);
|
|
36
|
+
const declared = declaredTools.get(tool.name);
|
|
37
|
+
if (!declared) {
|
|
38
|
+
throw new Error(`registered tool ${tool.name} (${tool.lane} / ${tool.scope}) has no lane coverage declaration`);
|
|
39
|
+
}
|
|
40
|
+
if (declared.lane !== tool.lane || declared.scope !== tool.scope) {
|
|
41
|
+
throw new Error(`tool ${tool.name} registration (${tool.lane} / ${tool.scope}) disagrees with its declaration ` +
|
|
42
|
+
`(${declared.lane} / ${declared.scope})`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const registered = new Set(tools.map((tool) => tool.name));
|
|
46
|
+
for (const [name] of declaredTools) {
|
|
47
|
+
if (!registered.has(name))
|
|
48
|
+
throw new Error(`coverage declaration claims unregistered tool ${name}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Full build-time guard: lane declarations ↔ generated/committed manifest ↔
|
|
53
|
+
* registered tool surface. Run by `scripts/coverage-manifest.ts --check` and
|
|
54
|
+
* by `test/coverage-consistency.test.ts`; it is intentionally outside the
|
|
55
|
+
* runtime import graph so the gate stays cycle-free.
|
|
56
|
+
*/
|
|
57
|
+
export function assertCoverageConsistency() {
|
|
58
|
+
assertDeclarationsAlignWithManifest(loadCoverageManifest());
|
|
59
|
+
assertToolSurfaceConsistent(buildToolRegistry());
|
|
60
|
+
assertContractBindings();
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=coverage-consistency.js.map
|