omc-core 0.4.36 → 0.4.37

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.
@@ -1,5 +1,5 @@
1
1
  import { buildChildTableDeleteRequest, buildSheetControlBinding } from "./childtable-field-contract.js";
2
- import { ticket11ScaffoldControlKeys, buildTicket11ScaffoldControls, ticket11DraftLayoutIsEmpty } from "./ticket11-test-form-draft.js";
2
+ import { ticket11ScaffoldControlKeys, buildTicket11ScaffoldControls, ticket11DraftLayoutIsEmpty } from "./form-scaffold-contract.js";
3
3
  export const CHILD_TABLE_SHEET201_ENDPOINTS = Object.freeze({
4
4
  fieldPublish: "/api/api/app/bizproperty/publish",
5
5
  formGet: "/api/api/app/bizsheet/get",
@@ -1,6 +1,6 @@
1
1
  import { CHILD_TABLE_CONTRACT } from "./childtable-field-contract.js";
2
2
  import { FORMULA_FIELD_CONTRACT } from "./formula-field-contract.js";
3
- import { ticket08TargetSchemaCode, isTicket08TargetSchema } from "./ticket08-test-model-mutation.js";
3
+ import { ticket08TargetSchemaCode, isTicket08TargetSchema } from "./field-probe-schema.js";
4
4
  export const FIELD_PROBE_MODES = Object.freeze(["rehearsal", "execute"]);
5
5
  export const FIELD_PROBE_KINDS = Object.freeze(["formula", "childtable"]);
6
6
  export const FIELD_PROBE_PHASES = Object.freeze(["field-create", "publish", "controls"]);
@@ -0,0 +1,19 @@
1
+ /**
2
+ * 字段探针目标模型 schema 契约(生产):omc_t08_<16hex> 生成/识别/断言。
3
+ * 原实现住在 ticket08 测试编排内;C2-lite 拆分——field-probe-runner 与测试岛屿共用。
4
+ */
5
+ import { randomUUID } from "node:crypto";
6
+ export const TICKET08_TARGET_SCHEMA_PATTERN = /^omc_t08_[0-9a-f]{16}$/;
7
+ const TICKET08_SCHEMA_CODE_PREFIX = "omc_t08_";
8
+ export function ticket08TargetSchemaCode() {
9
+ return `${TICKET08_SCHEMA_CODE_PREFIX}${randomUUID().replaceAll("-", "").slice(0, 16)}`;
10
+ }
11
+ export function isTicket08TargetSchema(targetSchemaCode) {
12
+ return TICKET08_TARGET_SCHEMA_PATTERN.test(targetSchemaCode);
13
+ }
14
+ export function assertTicket08TargetSchema(targetSchemaCode) {
15
+ if (!isTicket08TargetSchema(targetSchemaCode))
16
+ throw new Error("Ticket 08 targetSchemaCode must be a host-scoped omc_t08_<16-hex> schema within the CloudPivot 8.6.26 26-character code contract.");
17
+ return targetSchemaCode;
18
+ }
19
+ //# sourceMappingURL=field-probe-schema.js.map
@@ -0,0 +1,126 @@
1
+ /**
2
+ * 表单脚手架契约(生产):默认表单 Title/系统行/Group 三行脚手架的构建与解码。
3
+ * 原实现住在 ticket11 测试编排内;C2-lite 拆分——生产消费者
4
+ * (formula-controls / childtable-sheet201 契约)与测试岛屿共用此唯一事实源。
5
+ */
6
+ export const TICKET11_SCAFFOLD_SYSTEM_ROW = Object.freeze(["creater", "createdTime", "sequenceNo"]);
7
+ export const TICKET11_SCAFFOLD_SYSTEM_CONTROL_TYPES = Object.freeze({ creater: 101, createdTime: 104, sequenceNo: 100 });
8
+ /** The UID rule value is a designer/composer constant, not a per-model scheme reference (research §13.2). */
9
+ export const TICKET11_SCAFFOLD_SEQNO_UID_VALUE = "aa860e64ff2e4f86851a661d663ae358";
10
+ export const TICKET11_SCAFFOLD_GROUP_NAME = "业务标题";
11
+ export const TICKET11_SCAFFOLD_DATE_FORMAT = "YYYY-MM-DD HH:mm:ss";
12
+ /** One shared timestamp per composition mirrors the designer's Title<ts>/Group<ts> pattern (research §13.4). */
13
+ export function ticket11ScaffoldControlKeys(capturedAt) {
14
+ const epochMs = Date.parse(capturedAt);
15
+ if (!Number.isFinite(epochMs) || epochMs <= 0)
16
+ throw new Error("Ticket 11 scaffold composition requires the evidence capture clock; nothing ships on guessed layout keys.");
17
+ return Object.freeze({ titleKey: `Title${epochMs}`, groupKey: `Group${epochMs}` });
18
+ }
19
+ function deepFrozen(value) {
20
+ if (Array.isArray(value))
21
+ return Object.freeze(value.map(deepFrozen));
22
+ if (value && typeof value === "object")
23
+ return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, item]) => [key, deepFrozen(item)])));
24
+ return value;
25
+ }
26
+ /** The designer-standard scaffolding control map entries, mirroring the live agent_test_customer sample member-for-member (research §13.1). Deep-frozen: the composition is an immutable contract value. */
27
+ export function buildTicket11ScaffoldControls(input) {
28
+ return deepFrozen({
29
+ [input.titleKey]: {
30
+ key: input.titleKey,
31
+ type: 203,
32
+ options: {
33
+ name: input.formName,
34
+ name_i18n: { en: input.formName, zh: input.formName },
35
+ visible: true,
36
+ labelVisible: true,
37
+ span: 24,
38
+ labelAlign: "",
39
+ style: "",
40
+ tips: "",
41
+ dataItemName: "",
42
+ useDataTitle: true,
43
+ styleInfo: { height: 1, bgColor: "#ffffff", bgImg: "", textAlign: "center", fontSize: 28, textColor: "#111218", blod: true, italic: false, textCss: "", textCssSwitch: false },
44
+ },
45
+ },
46
+ creater: {
47
+ key: "creater",
48
+ type: TICKET11_SCAFFOLD_SYSTEM_CONTROL_TYPES.creater,
49
+ options: { name: "创建人", name_i18n: "", visible: true, labelVisible: true, span: 8, labelAlign: "", style: "", tips: "", dataItemName: "", widgetType: "", dataItemType: "", mappings: "", verifyFormula: "", defaultValue: "" },
50
+ },
51
+ createdTime: {
52
+ key: "createdTime",
53
+ type: TICKET11_SCAFFOLD_SYSTEM_CONTROL_TYPES.createdTime,
54
+ options: { name: "创建时间", name_i18n: "", visible: true, labelVisible: true, span: 8, labelAlign: "", style: "", tips: "", dataItemName: "", widgetType: "", dataItemType: "", format1: TICKET11_SCAFFOLD_DATE_FORMAT, format: TICKET11_SCAFFOLD_DATE_FORMAT, syncFormate: true, verifyFormula: "", defaultValue: "" },
55
+ },
56
+ sequenceNo: {
57
+ key: "sequenceNo",
58
+ type: TICKET11_SCAFFOLD_SYSTEM_CONTROL_TYPES.sequenceNo,
59
+ options: { name: "单据号", name_i18n: "", visible: true, labelVisible: true, span: 8, labelAlign: "", style: "", tips: "", dataItemName: "", widgetType: "", dataItemType: "", seqNoSetting: [{ type: "UID", value: TICKET11_SCAFFOLD_SEQNO_UID_VALUE }], formPropertyType: 100, verifyFormula: "", displayFormula: "", defaultValue: "" },
60
+ },
61
+ [input.groupKey]: {
62
+ key: input.groupKey,
63
+ type: 200,
64
+ options: { name: TICKET11_SCAFFOLD_GROUP_NAME, tips: "", expand: true, align: "", style: "", styleInfo: { fontSize: 18, textColor: "#111218", blod: true, italic: false, textCss: "", textCssSwitch: false }, span: 24 },
65
+ },
66
+ });
67
+ }
68
+ /** An auto-created default form decodes with an empty draft layout (research §2): draftViewJson null/[] and draftAttributesJson null/{}. */
69
+ export function ticket11DraftLayoutIsEmpty(form) {
70
+ let view = null;
71
+ let attributes = null;
72
+ try {
73
+ view = form.draftViewJson === null ? [] : JSON.parse(form.draftViewJson);
74
+ attributes = form.draftAttributesJson === null ? {} : JSON.parse(form.draftAttributesJson);
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ return Array.isArray(view) && view.length === 0 && typeof attributes === "object" && attributes !== null && !Array.isArray(attributes) && Object.keys(attributes).length === 0;
80
+ }
81
+ export function decodeTicket11ScaffoldPresence(form) {
82
+ if (form.draftViewJson === null || form.draftAttributesJson === null)
83
+ return { decodable: false, rowOrderMatchesContract: false };
84
+ let view;
85
+ let attributes;
86
+ try {
87
+ view = JSON.parse(form.draftViewJson);
88
+ attributes = JSON.parse(form.draftAttributesJson);
89
+ }
90
+ catch {
91
+ return { decodable: false, rowOrderMatchesContract: false };
92
+ }
93
+ if (!Array.isArray(view) || view.some((row) => !Array.isArray(row) || row.some((key) => typeof key !== "string")) || !attributes || typeof attributes !== "object" || Array.isArray(attributes))
94
+ return { decodable: false, rowOrderMatchesContract: false };
95
+ const rows = view;
96
+ const map = attributes;
97
+ const entry = (key) => {
98
+ const value = map[key];
99
+ return value && typeof value === "object" && !Array.isArray(value) && typeof value.type === "number" && value.options && typeof value.options === "object"
100
+ ? value
101
+ : undefined;
102
+ };
103
+ // The row contract is positional (research §13.5): row 0 is the single-key
104
+ // Title row, row 1 is exactly the system row, row 2 is the single-key Group
105
+ // row. Extra or duplicated scaffold rows decode as non-contract.
106
+ const isTitleKey = (key) => /^Title\d+$/.test(key);
107
+ const isGroupKey = (key) => /^Group\d+$/.test(key);
108
+ const titleShaped = rows.length >= 3 && rows[0].length === 1 && isTitleKey(rows[0][0]) && entry(rows[0][0]) !== undefined;
109
+ const systemShaped = rows.length >= 3 && rows[1].length === 3 && rows[1][0] === "creater" && rows[1][1] === "createdTime" && rows[1][2] === "sequenceNo";
110
+ const groupShaped = rows.length >= 3 && rows[2].length === 1 && isGroupKey(rows[2][0]) && entry(rows[2][0]) !== undefined;
111
+ if (!titleShaped || !systemShaped || !groupShaped)
112
+ return { decodable: false, rowOrderMatchesContract: false };
113
+ const title = entry(rows[0][0]);
114
+ const group = entry(rows[2][0]);
115
+ const systemTypes = rows[1].map((key) => entry(key)?.type ?? -1);
116
+ const scaffoldRowsOnly = rows.slice(0, 3).every((row, index) => index === 0 ? row.length === 1 && isTitleKey(row[0] ?? "") : index === 1 ? row.length === 3 : row.length === 1 && isGroupKey(row[0] ?? "")) && rows.slice(3).every((row) => row.length === 0 || (!isTitleKey(row[0] ?? "") && !isGroupKey(row[0] ?? "") && row[0] !== "creater" && row[0] !== "createdTime" && row[0] !== "sequenceNo"));
117
+ return Object.freeze({
118
+ decodable: true,
119
+ title: Object.freeze({ key: rows[0][0], type: title.type, useDataTitle: title.options.useDataTitle === true, name: title.options.name }),
120
+ systemRow: Object.freeze({ keys: Object.freeze([...rows[1]]), types: Object.freeze(systemTypes) }),
121
+ group: Object.freeze({ key: rows[2][0], type: group.type }),
122
+ rowOrder: Object.freeze(rows.map((row) => row.join("+"))),
123
+ rowOrderMatchesContract: scaffoldRowsOnly,
124
+ });
125
+ }
126
+ //# sourceMappingURL=form-scaffold-contract.js.map
@@ -1,6 +1,6 @@
1
1
  import { decodeDraftControl } from "./cloudpivot-low-code.js";
2
2
  import { buildFormulaFieldDeleteRequest } from "./formula-field-contract.js";
3
- import { buildTicket11ScaffoldControls, decodeTicket11ScaffoldPresence, ticket11DraftLayoutIsEmpty, ticket11ScaffoldControlKeys } from "./ticket11-test-form-draft.js";
3
+ import { buildTicket11ScaffoldControls, decodeTicket11ScaffoldPresence, ticket11DraftLayoutIsEmpty, ticket11ScaffoldControlKeys } from "./form-scaffold-contract.js";
4
4
  export const FORMULA_CONTROL_TYPE = 89;
5
5
  export const FORMULA_FORM_DRAFT_ENDPOINT = "/api/api/app/bizsheet/update";
6
6
  export const FORMULA_FORM_PUBLISH_ENDPOINT = "/api/api/app/bizsheet/publish";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omc-core",
3
- "version": "0.4.36",
3
+ "version": "0.4.37",
4
4
  "description": "Shared OMC capability kernel: capability contracts, governance, and decoders. Zero DSH dependencies.",
5
5
  "type": "module",
6
6
  "files": [
@@ -1,443 +0,0 @@
1
- // Ticket 16 (approval-readability issue 02): the conversational plan-of-record
2
- // with a single master approval for the standard Ticket 08→12 composite chain.
3
- //
4
- // Phase-0 mechanism decisions (recorded in the issue file):
5
- //
6
- // 1. HOW ONE APPROVAL BINDS MANY PACKAGES. Later-stage packages cannot exist
7
- // before earlier stages execute (each stage's evidence consumes the prior
8
- // stage's completed, behavior-validated execution report; package hashes
9
- // embed live baseline fingerprints), so literal pre-execution enumeration of
10
- // all five package ids/hashes is impossible without weakening the serial
11
- // evidence discipline. The master approval therefore binds the chain by its
12
- // DETERMINISTIC DERIVED IDENTITY, pre-minted by the Host at compose time:
13
- // one testRunId for the whole run plus the exact target identities
14
- // (omc_t08_<16-hex> schema, omc_t09_<16-hex> field, default sheet), the
15
- // routes, the pinned 8.6.26 capability contract hashes, the Knowledge Pack
16
- // digests, the exact scopes, and the declared staged-cleanup contracts. The
17
- // sha256 over that canonical enumeration is the single hash anchor carried
18
- // by the master approval frame. Every concrete package must then match its
19
- // enumerated slot EXACTLY to consume the grant, and each consumption binds
20
- // the concrete packageId/packageHash into the master record — so the
21
- // acceptance record shows the literal package id/hash per stage after the
22
- // fact. A package that is not enumerated refuses the grant: it can only
23
- // execute through its own per-package DSH approval frame (the unchanged
24
- // legacy/emergency path).
25
- //
26
- // 2. PLAN-TEXT PROVENANCE. The conversational plan-of-record is HOST-DERIVED:
27
- // it is rendered by this module from the immutable master plan record and
28
- // surfaced as the omc_master_plan_compose tool output (visible in the DSH
29
- // chat history). It is never accepted as an input anywhere; the approval
30
- // binds to the record and its hash anchor, not to any displayed text, so
31
- // model paraphrasing has no effect. The omc_master_plan_approve tool
32
- // re-renders both the plan text and the approval frame from the record and
33
- // byte-compares (verifyConversationalMasterPlan /
34
- // verifyMasterApprovalFrameHumanBlock) — the binding lives in the
35
- // structure, not in the display text.
36
- //
37
- // Ticket 17 (issue 03, grill 2026-09-05) — presentation-only format polish:
38
- // the plan-of-record renders as the approved markdown table (| # | 操作 |
39
- // 目标 | 说明 | with the staged cleanups as rows 6-7; boundary promises in
40
- // one header line; scope strings and per-stage contract hashes only in the
41
- // machine receipt below the plan text), with the approved compact-list
42
- // fallback for plain-text tool-output rendering; the master approval frame
43
- // is exactly three host-derived lines with the machine prefix deleted (new
44
- // contract, no archived-replay obligation — the legacy per-stage frames stay
45
- // byte-identical); stage results report one line per success with full
46
- // verbatim disclosure on failure/blocked/uncertain. Governance mechanics are
47
- // untouched.
48
- import { createHash, randomUUID } from "node:crypto";
49
- import { assertMasterApprovalFrameHumanBlock, renderMasterApprovalFrame, verifyMasterApprovalFrameHumanBlock } from "./approval-frame-readability.js";
50
- import { activeFieldFamily, fieldFamilyMembers } from "./families/registry.js";
51
- /** The DSH tool that carries the single master approval frame. */
52
- export const MASTER_APPROVAL_TOOL_NAME = "omc_master_plan_approve";
53
- /**
54
- * Byte-stable line prefix the maintainer runner matches for the single master
55
- * frame (Ticket 17: the machine prefix line was deleted — the frame's first
56
- * line IS the approval summary, so this prefix anchors it).
57
- */
58
- export const MASTER_APPROVAL_REASON_PREFIX = "批准执行 ";
59
- const HEX64 = /^[a-f0-9]{64}$/i;
60
- const UUID_V4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
61
- const TARGET_SCHEMA = /^omc_t08_[0-9a-f]{16}$/;
62
- const TARGET_FIELD = /^omc_t09_[0-9a-f]{16}$/;
63
- const FOLDER_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{7,63}$/;
64
- /**
65
- * Composes the immutable master plan record: pre-mints the whole chain's
66
- * derived identity (one testRunId, fixture schema/field codes, default sheet)
67
- * and derives the single hash anchor over the canonical enumeration.
68
- */
69
- export function composeMasterPlan(input) {
70
- if (input.environmentName !== "test" || input.applicationCode !== "test")
71
- throw new Error("The master delivery plan is restricted to the named test environment and its dedicated test application.");
72
- if (!FOLDER_ID.test(input.folder.id) || typeof input.folder.name !== "string" || !input.folder.name.trim())
73
- throw new Error("The master delivery plan requires the exact discovered folder identity.");
74
- const testRunId = input.ids?.testRunId ?? randomUUID();
75
- const targetSchemaCode = input.ids?.targetSchemaCode ?? `omc_t08_${randomUUID().replaceAll("-", "").slice(0, 16)}`;
76
- // Ticket 25: stage 2 creates the ACTIVE family's typed fields (one code
77
- // per registered member, in governed order) and every later stage binds the
78
- // same ordered set.
79
- const memberCount = fieldFamilyMembers().length;
80
- const targetFieldCodes = input.ids?.targetFieldCodes ?? Array.from({ length: memberCount }, () => `omc_t09_${randomUUID().replaceAll("-", "").slice(0, 16)}`);
81
- if (!UUID_V4.test(testRunId) || !TARGET_SCHEMA.test(targetSchemaCode) || targetFieldCodes.length !== memberCount || targetFieldCodes.some((code) => !TARGET_FIELD.test(code)) || new Set(targetFieldCodes).size !== memberCount)
82
- throw new Error("The master delivery plan identities must be the host-scoped testRunId, omc_t08 schema code, and one distinct omc_t09 family field code per active family member.");
83
- const stageOrder = ["ticket08-test-model-mutation", "ticket09-test-field-mutation", "ticket10-test-field-publish", "ticket11-test-form-draft", "ticket20-test-data-rule", "ticket12-test-form-publish"];
84
- const humanCapabilities = {
85
- "ticket08-test-model-mutation": "ticket08-model-create",
86
- "ticket09-test-field-mutation": "ticket09-field-create",
87
- "ticket10-test-field-publish": "ticket10-field-publish",
88
- "ticket11-test-form-draft": "ticket11-form-draft",
89
- "ticket20-test-data-rule": "ticket20-data-rule",
90
- "ticket12-test-form-publish": "ticket12-form-publish",
91
- };
92
- const stages = stageOrder.map((route, index) => {
93
- const pin = input.stagePins[route];
94
- if (!pin || pin.route !== route || !pin.capabilityId || !HEX64.test(pin.contractHash) || !HEX64.test(pin.knowledgePackDigest))
95
- throw new Error(`The master delivery plan requires the pinned capability, contract hash, and Knowledge Pack digest for ${route}.`);
96
- const slot = {
97
- stage: (index + 1),
98
- route,
99
- capabilityId: pin.capabilityId,
100
- humanCapability: humanCapabilities[route],
101
- targetSchemaCode,
102
- targetFieldCodes: index >= 1 ? targetFieldCodes : undefined,
103
- targetSheetCode: index >= 3 ? targetSchemaCode : undefined,
104
- scope: index === 0 ? `cloudpivot:model:${targetSchemaCode}` : index <= 2 ? `cloudpivot:fields:${targetSchemaCode}/${targetFieldCodes.join("+")}` : index === 4 ? `cloudpivot:rule:${targetSchemaCode}/${targetFieldCodes[activeFieldFamily().ruleDemoTargetPosition - 1]}` : `cloudpivot:form:${targetSchemaCode}/${targetSchemaCode}`,
105
- contractHash: pin.contractHash,
106
- knowledgePackDigest: pin.knowledgePackDigest,
107
- };
108
- return slot;
109
- });
110
- const fieldCleanupPin = input.cleanupPins["ticket10-test-field-cleanup"];
111
- const modelCleanupPin = input.cleanupPins["ticket08-test-model-cleanup"];
112
- if (!fieldCleanupPin || !modelCleanupPin)
113
- throw new Error("The master delivery plan requires both declared staged-cleanup pins.");
114
- const cleanups = [
115
- { route: "ticket10-test-field-cleanup", followsStage: 3, capabilityId: fieldCleanupPin.capabilityId, contractHash: fieldCleanupPin.contractHash },
116
- { route: "ticket08-test-model-cleanup", followsStage: 1, capabilityId: modelCleanupPin.capabilityId, contractHash: modelCleanupPin.contractHash },
117
- ];
118
- for (const cleanup of cleanups) {
119
- if (!cleanup.capabilityId || !HEX64.test(cleanup.contractHash))
120
- throw new Error(`The master delivery plan requires the pinned cleanup capability and contract hash for ${cleanup.route}.`);
121
- }
122
- const createdAt = (input.now ?? (() => new Date()))().toISOString();
123
- const record = {
124
- masterPlanId: `master-${createHash("sha256").update(`${createdAt}:${testRunId}:${targetSchemaCode}:${targetFieldCodes.join("+")}`).digest("hex").slice(0, 24)}`,
125
- createdAt,
126
- environmentName: input.environmentName,
127
- applicationCode: input.applicationCode,
128
- folder: { id: input.folder.id, name: input.folder.name },
129
- testRunId,
130
- stages,
131
- cleanups,
132
- masterHash: "",
133
- status: "proposed",
134
- consumed: [],
135
- ledgerHash: "",
136
- };
137
- const sealed = { ...record, masterHash: masterPlanHash(record) };
138
- return { ...sealed, ledgerHash: masterLedgerHash(sealed.approval, sealed.consumed) };
139
- }
140
- function canonical(value) {
141
- return JSON.stringify(sort(value));
142
- }
143
- function sort(value) {
144
- if (Array.isArray(value))
145
- return value.map(sort);
146
- if (value && typeof value === "object")
147
- return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, sort(item)]));
148
- return value;
149
- }
150
- /** The hash anchor is computed over the canonical enumeration only — never over display text. */
151
- function masterPlanHash(record) {
152
- return createHash("sha256").update(canonical({
153
- testRunId: record.testRunId,
154
- folder: record.folder,
155
- stages: record.stages.map((slot) => ({ stage: slot.stage, route: slot.route, capabilityId: slot.capabilityId, targetSchemaCode: slot.targetSchemaCode, targetFieldCodes: slot.targetFieldCodes ?? null, targetSheetCode: slot.targetSheetCode ?? null, scope: slot.scope, contractHash: slot.contractHash, knowledgePackDigest: slot.knowledgePackDigest })),
156
- cleanups: record.cleanups.map((cleanup) => ({ route: cleanup.route, followsStage: cleanup.followsStage, capabilityId: cleanup.capabilityId, contractHash: cleanup.contractHash })),
157
- })).digest("hex");
158
- }
159
- /** Integrity root over the approval binding plus the consumption ledger (the masterHash stays fixed at approval time). */
160
- function masterLedgerHash(approval, consumed) {
161
- return createHash("sha256").update(canonical({
162
- approval: approval ? { callId: approval.callId, approvedAt: approval.approvedAt } : null,
163
- consumed: consumed.map((item) => ({ kind: item.kind, route: item.route, stage: item.stage, planApprovalId: item.planApprovalId, packageId: item.packageId ?? null, packageHash: item.packageHash ?? null, consumedAt: item.consumedAt })),
164
- })).digest("hex");
165
- }
166
- /** Re-derives both roots and fails any record whose enumeration or approval/consumption ledger diverges. */
167
- export function verifyMasterPlanRecord(record) {
168
- if (!record || !Array.isArray(record.stages) || record.stages.length !== 6 || !Array.isArray(record.cleanups) || !Array.isArray(record.consumed))
169
- throw new Error("Master plan record is malformed.");
170
- if (record.masterHash !== masterPlanHash(record))
171
- throw new Error("Master plan record diverges from its own hash anchor.");
172
- if (record.ledgerHash !== masterLedgerHash(record.approval, record.consumed))
173
- throw new Error("Master plan record approval or consumption ledger diverges from its integrity root.");
174
- if (!UUID_V4.test(record.testRunId))
175
- throw new Error("Master plan record testRunId is malformed.");
176
- if (record.status !== "proposed" && record.status !== "approved")
177
- throw new Error("Master plan record status is malformed.");
178
- if (record.status === "approved" && (!record.approval || !record.approval.callId || !Number.isFinite(Date.parse(record.approval.approvedAt))))
179
- throw new Error("Approved master plan record is missing its approval binding.");
180
- if (/(password|token|cookie|client[_-]?secret|authorization|api[_-]?key|private[_-]?key|bearer\s|-----begin|raw[_-]?(request|response|log))/i.test(canonical(record)))
181
- throw new Error("Master plan record contains prohibited sensitive data.");
182
- }
183
- /** Marks the master plan approved under the one DSH approval call. */
184
- export function approveMasterPlan(record, approval) {
185
- verifyMasterPlanRecord(record);
186
- if (record.status === "approved")
187
- throw new Error("Master plan is already approved.");
188
- if (!approval.callId)
189
- throw new Error("Master plan approval requires the DSH call ID provenance.");
190
- const approved = { ...record, status: "approved", approval: { callId: approval.callId, approvedAt: approval.approvedAt ?? new Date().toISOString() } };
191
- if (approved.masterHash !== masterPlanHash(approved))
192
- throw new Error("Approved master plan diverges from its hash anchor.");
193
- return { ...approved, ledgerHash: masterLedgerHash(approved.approval, approved.consumed) };
194
- }
195
- function candidateMatchesSlot(slot, candidate) {
196
- return slot.route === candidate.route
197
- && slot.targetSchemaCode === candidate.targetSchemaCode
198
- && (slot.targetFieldCodes ? slot.targetFieldCodes.join("+") : undefined) === (candidate.targetFieldCodes ? candidate.targetFieldCodes.join("+") : undefined)
199
- && (slot.targetSheetCode ?? undefined) === (candidate.targetSheetCode ?? undefined);
200
- }
201
- /** Exact plan-membership: an approved master plan's slot matches only the identical derived identity. */
202
- export function matchMasterStageSlot(record, candidate) {
203
- verifyMasterPlanRecord(record);
204
- if (record.status !== "approved")
205
- return undefined;
206
- if (record.testRunId !== candidate.testRunId)
207
- return undefined;
208
- return record.stages.find((slot) => candidateMatchesSlot(slot, candidate));
209
- }
210
- /** Exact plan-membership for the declared staged cleanups (lineage: same testRunId + the followed stage's exact targets). */
211
- export function matchMasterCleanupSlot(record, candidate) {
212
- verifyMasterPlanRecord(record);
213
- if (record.status !== "approved")
214
- return undefined;
215
- if (record.testRunId !== candidate.testRunId)
216
- return undefined;
217
- for (const cleanup of record.cleanups) {
218
- if (cleanup.route !== candidate.route)
219
- continue;
220
- const slot = record.stages.find((stage) => stage.stage === cleanup.followsStage);
221
- if (!slot)
222
- continue;
223
- if (slot.targetSchemaCode === candidate.targetSchemaCode
224
- && (slot.targetFieldCodes ? slot.targetFieldCodes.join("+") : undefined) === (candidate.targetFieldCodes ? candidate.targetFieldCodes.join("+") : undefined)
225
- && (slot.targetSheetCode === undefined || candidate.targetSheetCode === undefined || slot.targetSheetCode === candidate.targetSheetCode))
226
- return { slot, cleanup };
227
- }
228
- return undefined;
229
- }
230
- /** Records one grant consumption; identical re-consumption is a no-op, divergent re-consumption is refused. */
231
- export function recordMasterPlanConsumption(record, consumption) {
232
- verifyMasterPlanRecord(record);
233
- if (record.status !== "approved")
234
- throw new Error("Master plan consumption requires the approved master plan.");
235
- if (!consumption.planApprovalId || !Number.isFinite(Date.parse(consumption.consumedAt)))
236
- throw new Error("Master plan consumption is malformed.");
237
- if (consumption.packageHash && !HEX64.test(consumption.packageHash))
238
- throw new Error("Master plan consumption package hash is malformed.");
239
- const existing = record.consumed.find((item) => item.kind === consumption.kind && item.route === consumption.route && item.stage === consumption.stage);
240
- if (existing) {
241
- if (existing.planApprovalId !== consumption.planApprovalId || (existing.packageId ?? undefined) !== (consumption.packageId ?? undefined))
242
- throw new Error("Master plan grant was already consumed by a different approval or package; operator reconcile is required.");
243
- return record;
244
- }
245
- const consumed = [...record.consumed, consumption];
246
- return { ...record, consumed, ledgerHash: masterLedgerHash(record.approval, consumed) };
247
- }
248
- /** The human block input derived from the immutable record — the only source both renderers share. */
249
- export function masterApprovalHumanBlockInput(record) {
250
- verifyMasterPlanRecord(record);
251
- return {
252
- stages: record.stages.map((slot) => ({
253
- capability: slot.humanCapability,
254
- identities: [
255
- slot.targetSchemaCode,
256
- ...(slot.humanCapability === "ticket20-data-rule"
257
- ? [slot.targetFieldCodes?.[activeFieldFamily().ruleDemoTargetPosition - 1]]
258
- : slot.targetFieldCodes ?? []),
259
- ...(slot.targetSheetCode && (slot.humanCapability === "ticket11-form-draft" || slot.humanCapability === "ticket12-form-publish") ? [slot.targetSheetCode] : []),
260
- ],
261
- scope: slot.scope,
262
- })),
263
- cleanupCount: record.cleanups.length,
264
- environment: record.environmentName,
265
- application: record.applicationCode,
266
- folderId: record.folder.id,
267
- folderName: record.folder.name,
268
- anchorHash: record.masterHash,
269
- testRunId: record.testRunId,
270
- };
271
- }
272
- // --- Ticket 17 (issue 03): the plan-of-record's approved reading shapes ------
273
- /**
274
- * Stage-0 render decision (grill decision 3): VERIFIED 2026-09-05 with
275
- * screenshot evidence (research/stage0-broken-markdown-table*.png) that the
276
- * DSH web UI renders tool-output text blocks as PLAIN TEXT — the markdown
277
- * table's pipes and divider row appear verbatim inline (no table/grid
278
- * element is created), so the table shape would ship broken. The plan
279
- * therefore ships as the approved compact indented list (divider + indented
280
- * `N 操作 目标 说明` lines). If a future DSH build renders markdown in tool
281
- * outputs, flip this to "markdown-table" for the table shape.
282
- */
283
- export const MASTER_PLAN_RENDER_MODE = "compact-list";
284
- /** Reading-shortening helper: fixture codes keep a cross-checkable prefix; the full codes live in the machine receipt below the plan. */
285
- function fixtureCodeDisplayPrefix(code) {
286
- return `${code.slice(0, code.indexOf("_", 4) + 5)}…`;
287
- }
288
- /**
289
- * ONE Host-owned registry per stage: the plan table/list row AND the stage
290
- * result line both derive from it, so the two surfaces can never drift.
291
- * Ticket 03b: the family-varying cells (the field-create row's short label
292
- * and qualifiers, the form-draft row's control kinds, the rule row's demo
293
- * target) derive from the ACTIVE family registration
294
- * (src/families/<family>.ts) — a future family rung never edits this
295
- * table.
296
- */
297
- const FAMILY_PLAN_ROW = activeFieldFamily().planRow;
298
- const FIELD_CREATE_NOTE = [FAMILY_PLAN_ROW.familyShortLabel, ...FAMILY_PLAN_ROW.fieldCreateQualifiers].join("·");
299
- const FIELD_CREATE_DECODE = `${FAMILY_PLAN_ROW.familyShortLabel}·${FAMILY_PLAN_ROW.fieldSetLabel}`;
300
- const FORM_DRAFT_NOTE = `脚手架+${FAMILY_PLAN_ROW.formDraftControlKinds}`;
301
- const RULE_NOTE = `设计器规则面·${FAMILY_PLAN_ROW.ruleTargetLabel}`;
302
- const MASTER_STAGE_ROWS = {
303
- "ticket08-model-create": { operation: "创建模型", note: "LIST 测试夹具", decode: "LIST 测试夹具", readback: "权威回读通过" },
304
- "ticket09-field-create": { operation: "创建字段", note: FIELD_CREATE_NOTE, decode: FIELD_CREATE_DECODE, readback: "两态读回通过" },
305
- "ticket10-field-publish": { operation: "发布字段", note: `模型级发布·${FAMILY_PLAN_ROW.fieldSetLabel}`, decode: "模型级发布", readback: "两态读回通过" },
306
- "ticket11-form-draft": { operation: "表单草稿", note: FORM_DRAFT_NOTE, decode: "脚手架+类型化控件", readback: "草稿保存·发布态不变" },
307
- "ticket20-data-rule": { operation: "配置必填规则", note: RULE_NOTE, decode: "必填规则行(dataRuleType=4)", readback: "规则列表解码通过" },
308
- "ticket12-form-publish": { operation: "发布表单", note: "模型级发布", decode: "模型级发布", readback: "发布历史解码通过" },
309
- };
310
- const MASTER_CLEANUP_ROWS = {
311
- "ticket10-test-field-cleanup": { operation: "清理·删除字段", note: "自动,两态回读确认", decode: "自动", readback: "两态精确缺失" },
312
- "ticket08-test-model-cleanup": { operation: "清理·删除模型", note: "自动,级联+精确缺失", decode: "自动", readback: "级联+精确缺失" },
313
- };
314
- function masterStageRow(capability) {
315
- const row = MASTER_STAGE_ROWS[capability];
316
- if (!row)
317
- throw new Error(`The conversational plan has no Host-owned stage row for ${capability}.`);
318
- return row;
319
- }
320
- /** The fixed step sequence of the authorized chain: stages 1-6 then the two declared cleanups. */
321
- const MASTER_RESULT_SEQUENCE = [
322
- ...Object.values(MASTER_STAGE_ROWS),
323
- MASTER_CLEANUP_ROWS["ticket10-test-field-cleanup"],
324
- MASTER_CLEANUP_ROWS["ticket08-test-model-cleanup"],
325
- ];
326
- /** The eight step labels, exported for the maintainer runner's prompt legend (single source with the plan rows). */
327
- export const MASTER_STAGE_RESULT_TITLES = MASTER_RESULT_SEQUENCE.map((row) => row.operation);
328
- /** Derives the eight approved plan rows from the immutable record (host-owned fixed texts + truncated codes). */
329
- function masterPlanTableRows(record) {
330
- const rows = [];
331
- record.stages.forEach((slot, index) => {
332
- const row = masterStageRow(slot.humanCapability);
333
- rows.push({
334
- number: index + 1,
335
- operation: row.operation,
336
- target: index === 0 ? fixtureCodeDisplayPrefix(slot.targetSchemaCode) : index === 1 ? FAMILY_PLAN_ROW.fieldSetLabel : index === 2 ? `同上${FAMILY_PLAN_ROW.fieldSetLabel}` : index === 4 ? `同上${FAMILY_PLAN_ROW.fieldSetLabel}` : "默认表单",
337
- note: row.note,
338
- });
339
- });
340
- const fieldCleanup = record.cleanups.find((cleanup) => cleanup.route === "ticket10-test-field-cleanup");
341
- const modelCleanup = record.cleanups.find((cleanup) => cleanup.route === "ticket08-test-model-cleanup");
342
- const fieldCode = record.stages[1].targetFieldCodes?.[0] ?? "";
343
- const schemaCode = record.stages[0].targetSchemaCode;
344
- if (fieldCleanup)
345
- rows.push({ number: 7, operation: MASTER_CLEANUP_ROWS[fieldCleanup.route].operation, target: FAMILY_PLAN_ROW.fieldSetLabel, note: MASTER_CLEANUP_ROWS[fieldCleanup.route].note });
346
- if (modelCleanup)
347
- rows.push({ number: 8, operation: MASTER_CLEANUP_ROWS[modelCleanup.route].operation, target: fixtureCodeDisplayPrefix(schemaCode), note: MASTER_CLEANUP_ROWS[modelCleanup.route].note });
348
- return rows;
349
- }
350
- /** Renders the conversational plan-of-record displayed in the DSH chat (host output; never an input). */
351
- export function renderConversationalMasterPlan(record) {
352
- verifyMasterPlanRecord(record);
353
- const header = [
354
- `交付方案 · ${record.environmentName} 环境 · ${record.folder.name} · 方案编号 ${record.masterPlanId} · testRunId ${record.testRunId.slice(0, 8)}…`,
355
- "全程绝不 app publish、绝不 Environment release;结果逐档在对话内汇报",
356
- ];
357
- const rows = masterPlanTableRows(record);
358
- if (MASTER_PLAN_RENDER_MODE === "markdown-table") {
359
- return [
360
- ...header,
361
- "",
362
- "| # | 操作 | 目标 | 说明 |",
363
- "|---|------|------|------|",
364
- ...rows.map((row) => `| ${row.number} | ${row.operation} | ${row.target} | ${row.note} |`),
365
- ].join("\n");
366
- }
367
- return [
368
- ...header,
369
- "──────────────────────────────",
370
- ...rows.map((row) => ` ${row.number} ${row.operation} ${row.target} ${row.note}`),
371
- ].join("\n");
372
- }
373
- /** Byte-exact gate for plan text surfaced in the conversation (mismatch is rejected). */
374
- export function verifyConversationalMasterPlan(text, record) {
375
- if (typeof text !== "string" || text !== renderConversationalMasterPlan(record))
376
- throw new Error("Conversational plan text diverges from the immutable master plan record.");
377
- }
378
- /**
379
- * The single master approval frame: exactly three host-derived lines (summary
380
- * / impact+boundary / anchor+guidance). The machine prefix was deleted
381
- * (Ticket 17, grill decision 6) — the binding is structural (this approve
382
- * tool call plus the plan hash sealed in the record), so the frame is pure
383
- * display; the 8-hex anchor prefix stays for human cross-check.
384
- */
385
- export function formatMasterPlanApprovalFrame(record) {
386
- verifyMasterPlanRecord(record);
387
- if (record.status !== "proposed")
388
- throw new Error("The master approval frame is rendered from a proposed master plan record.");
389
- const input = masterApprovalHumanBlockInput(record);
390
- const frame = renderMasterApprovalFrame(input);
391
- assertMasterApprovalFrameHumanBlock(frame);
392
- verifyMasterApprovalFrameHumanBlock(frame, input);
393
- if (/(payload|credential|password|token|authorization|api[_-]?key|private[_-]?key|raw[_-]?(request|response|log))/i.test(frame))
394
- throw new Error("Master approval frame contains prohibited sensitive data.");
395
- return frame;
396
- }
397
- /**
398
- * The machine receipt: the full binding payload (identities, scopes, per-stage
399
- * contract hashes, cleanup declarations, consumption ledger) that used to ride
400
- * as a JSON tail on the compose tool output. Ticket 19 folds the
401
- * plan-json-tail-to-receipt backlog seed: the compose surface renders the
402
- * plan-of-record list ONLY, and this receipt travels on the master approval
403
- * result (and the acceptance report). The binding stays structural — the
404
- * approve tool call plus the record's hash anchor — regardless of where the
405
- * JSON renders.
406
- */
407
- export function masterPlanReceipt(record) {
408
- verifyMasterPlanRecord(record);
409
- return {
410
- masterPlanId: record.masterPlanId,
411
- masterHash: record.masterHash,
412
- status: record.status,
413
- testRunId: record.testRunId,
414
- environment: record.environmentName,
415
- application: record.applicationCode,
416
- folder: record.folder,
417
- targetSchemaCode: record.stages[0].targetSchemaCode,
418
- targetFieldCodes: record.stages[1].targetFieldCodes,
419
- targetSheetCode: record.stages[3].targetSheetCode,
420
- stages: record.stages.map((slot) => ({ stage: slot.stage, route: slot.route, capabilityId: slot.capabilityId, scope: slot.scope, contractHash: slot.contractHash, knowledgePackDigest: slot.knowledgePackDigest })),
421
- cleanups: record.cleanups.map((cleanup) => ({ route: cleanup.route, followsStage: cleanup.followsStage, capabilityId: cleanup.capabilityId, contractHash: cleanup.contractHash })),
422
- ...(record.approval ? { approval: record.approval } : {}),
423
- consumed: record.consumed,
424
- };
425
- }
426
- /**
427
- * Grill decision 7: a successful stage reports exactly one line
428
- * (✓ n/8 操作 · 关键解码 · 回读通过); a failed/blocked/uncertain stage
429
- * reports the ✗ headline AND returns the complete result for verbatim
430
- * disclosure. The operation/decode/readback texts derive from the same
431
- * Host-owned row registry as the plan rows, so the plan and the result
432
- * lines can never drift.
433
- */
434
- export function summarizeMasterStageResult(stage, result) {
435
- const row = MASTER_RESULT_SEQUENCE[stage - 1];
436
- const value = (result ?? {});
437
- const ok = value.status === "completed" && (!value.behavior || value.behavior.status === undefined || value.behavior.status === "validated");
438
- if (ok)
439
- return { ok: true, line: `✓ ${stage}/8 ${row.operation} · ${row.decode} · ${row.readback}` };
440
- const shortReason = typeof value.status === "string" && value.status ? value.status : "结果异常";
441
- return { ok: false, line: `✗ ${stage}/8 ${row.operation} · ${shortReason} → 完整结果与停工说明全文贴出`, fullDisclosure: JSON.stringify(result ?? null, null, 2) };
442
- }
443
- //# sourceMappingURL=master-plan-approval.js.map