@tea-agent/loop-agent 0.38.0 → 0.39.0-next.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/CHANGELOG.md +79 -0
- package/dist/build-stamp.json +3 -3
- package/dist/commands/init-upgrade.js +97 -19
- package/dist/commands/init.js +14 -2
- package/dist/executors/dag-pi-executor.js +1 -0
- package/dist/governance/manifest-types.js +2 -0
- package/dist/infrastructure/harness/atomic-write.js +2 -1
- package/dist/shared/openspec-spec.js +76 -0
- package/dist/task/config-types.js +21 -0
- package/dist/task/frontend-project-capability.js +292 -38
- package/dist/worker/console/chat/turn-process.js +7 -20
- package/dist/worker/console/harness-pi-model-matrix.js +146 -0
- package/dist/worker/console/operator-actions.js +49 -0
- package/dist/worker/console/operator-user-error.js +19 -0
- package/dist/worker/console/pi-readiness.js +7 -1
- package/dist/worker/console/security.js +20 -1
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DSvF6RXQ.js → abnfDiagram-N423BO3Z-B1E-cK2K.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DuI4ogtJ.js → arc-BGzjO9rd.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DdBbgD4y.js → architectureDiagram-T3A2C74G-DLdhmjAc.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-xQC2HELm.js → blockDiagram-VBNYF7ZC-MYV8D865.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DPMSgwIZ.js → c4Diagram-5PPSVZJV-BzDorEwd.js} +1 -1
- package/dist/worker/console/static/assets/channel-jVvdIfCV.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-WTEKTxin.js → chunk-2GRJ4B5K-C1X8DD9m.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B6vZ0Pv_.js → chunk-2Q5K7J3B-_b3zpUSR.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BrerNAix.js → chunk-5RXB4S5H-B1nq6qNo.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D_ch2aaI.js → chunk-5VM5RSS4-BHy2jQUr.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-VvfacNrq.js → chunk-6Q2QTUOP-BDoLBPGY.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B-uGFIg2.js → chunk-GF5L2VYU-SDhf5BHa.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BlysCtod.js → chunk-JWPE2WC7-B2X9WLQ5.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BjAUAfcT.js → chunk-KBJHAD2P-Y1pEsL6r.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BeZnBmb-.js → chunk-RYQCIY6F-BhlwtGnw.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Bbfe3UJG.js → chunk-XXDRQBXY-lFslfIU9.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-VqdMg0Mg.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-VqdMg0Mg.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BKJQnYVM.js → cose-bilkent-JH36ORCC-ms_QSyzO.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B1wtI3hU.js → cynefin-VYW2F7L2-DVBoJPNo.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DCF6DBSu.js → cynefinDiagram-MW4NZA55-ClesFZ8l.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJyNGRlP.js → dagre-VZM6K2ZE-CwcFFdXS.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B7j1-VsZ.js → diagram-7IWD3JNH-CK6fAQka.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BREJ140a.js → diagram-B4RE2ZJO-CxBy8J_n.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CDPHmHGH.js → diagram-LBJQPF4R-Cn6E66dO.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BtZLrez1.js → diagram-Q27KOJAE-Cep_FINW.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-mzybu8oU.js → diagram-UB23O5K3-gzv9ZY8Y.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Dk3I2uO6.js → ebnfDiagram-BXEA7PRR-CZVlOlaj.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BT97u_tQ.js → erDiagram-JOGREHBK-EGJvKAH5.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CMOtYzui.js → flowDiagram-UKHOOZJN-DhfPgO79.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiNydkuY.js → ganttDiagram-PKOTCBZU-BzJfLMVN.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js → gitGraphDiagram-DS77QQ5N-C5uHg3cm.js} +1 -1
- package/dist/worker/console/static/assets/{index-DQxiso1w.js → index-0DWC8Vk9.js} +67 -67
- package/dist/worker/console/static/assets/index-DAouGklf.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-AsGIGKKB.js → infoDiagram-6WML65LV-BFEs1rSc.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BL8ITZh3.js → ishikawaDiagram-WSZJBQD7-DOY3aNcc.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-VPKAUX8M.js → journeyDiagram-NVQOT4AX-CvEVBETJ.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BL56hfGp.js → kanban-definition-27J2QSJJ-CvRuqo46.js} +1 -1
- package/dist/worker/console/static/assets/{linear-BmSG3nIZ.js → linear-B8zO1HYx.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-DWGK9kwP.js → mermaid.core-FLK4IOPu.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS--vvKLjo0.js → mindmap-definition-FAOFIHXS-CwQ-RWrj.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B41mxb2-.js → pegDiagram-VL7TDLO6-H_S6wWna.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DTlu77sw.js → pieDiagram-7S7Q4E2Y-CZUOcDQQ.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bk5A7alv.js → quadrantDiagram-CIZ2JOQS-CeaPaSv9.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Csf9jzQy.js → railroadDiagram-AXF67PYL-Cpcs9ua-.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CeptoSNx.js → requirementDiagram-LRYGKXZP-D0_Aeyk6.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-A-DLCLer.js → sankeyDiagram-W5VNT64P-73kWjUq_.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-22VbPdOI.js → sequenceDiagram-SI44F4Z6-41K29Ot4.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DWvAyBC9.js → sizeCapture-X5ZJPWSS-D1nO0oh9.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-qICZ75yT.js → stateDiagram-OKZ733FA-C77XHgDB.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B9GimbZV.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-xi7lcMyp.js → swimlanes-SLNWSIFB-DC0D7jEO.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-yxfxvdqm.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Bu6Nl6O2.js → timeline-definition-Z64GVDOM-BLDHd_Uf.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BzMz4Bn9.js → vennDiagram-T6HMQDX7-eh6Y_q96.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CxaJnzc5.js → wardleyDiagram-T6FBY63Y-Cj4Cy9tr.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-5JK-omQK.js → xychartDiagram-ELKLHX3M-DUIjXY1m.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +51 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +14 -2
- package/dist/worker/console/static-src/operator-chat/mutation-gate.js +45 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +15 -0
- package/dist/workflows/dag/failure-routing.js +9 -4
- package/dist/workflows/dag/frontend-implementation-contract.js +118 -0
- package/dist/workflows/dag/frontend-plan-render.js +90 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +411 -20
- package/dist/workflows/dag/init-hybrid.js +271 -26
- package/dist/workflows/dag/lifecycle.js +4 -0
- package/dist/workflows/dag/node-execution.js +37 -7
- package/dist/workflows/dag/report.js +6 -0
- package/dist/workflows/dag/retry-policy.js +15 -0
- package/dist/workflows/dag/types.js +49 -4
- package/dist/workflows/dag/validate.js +7 -2
- package/docs/templates/frontend-design-contract.md +40 -14
- package/docs/templates/frontend-implementation-contract.schema.json +9 -2
- package/docs/templates/frontend-task-constraints.md +40 -13
- package/docs/templates/frontend-task-requirement.md +54 -20
- package/docs/templates/harness.schema.json +8 -0
- package/harness.json +11 -2
- package/package.json +1 -1
- package/skills/frontend-design-review/references/review-checklist.md +2 -0
- package/skills/frontend-implementation/references/design-spec.md +16 -19
- package/skills/frontend-implementation/references/node-contracts.md +15 -11
- package/dist/worker/console/static/assets/channel-DLVz9TYW.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeAuGcO9.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeAuGcO9.js +0 -1
- package/dist/worker/console/static/assets/index-_j0Hzo69.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DzKvfHTX.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cxqr_qM1.js +0 -8
|
@@ -5,7 +5,73 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { readFile, readdir, stat } from "node:fs/promises";
|
|
7
7
|
import path from "node:path";
|
|
8
|
-
import {
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { DEFAULT_OPENSPEC_GOVERNANCE_ROOT, MAX_OPENSPEC_CANDIDATE_PATHS, OPENSPEC_SPEC_DIRS, OPENSPEC_SPEC_EXT_RE, isGovernanceSubtreePath, isOpenspecSpecFilePath, } from "../shared/openspec-spec.js";
|
|
10
|
+
const classifiedPathSchema = z
|
|
11
|
+
.string()
|
|
12
|
+
.refine((candidate) => isOpenspecSpecFilePath(candidate), "classified path must be a repo-relative supported spec file under openspec/schemas/, openspec/project-specs/, or ai_workspace/");
|
|
13
|
+
/**
|
|
14
|
+
* Stable semantic partition of `designEvidence.normativePaths`. Keys are a
|
|
15
|
+
* generic directory convention (not project-specific file names); the same
|
|
16
|
+
* repository therefore yields the same classification on repeated probes.
|
|
17
|
+
*/
|
|
18
|
+
export const frontendDesignClassifiedSchema = z
|
|
19
|
+
.object({
|
|
20
|
+
schemas: z.array(classifiedPathSchema),
|
|
21
|
+
codeTemplate: z.array(classifiedPathSchema),
|
|
22
|
+
rule: z
|
|
23
|
+
.object({
|
|
24
|
+
api: z.array(classifiedPathSchema),
|
|
25
|
+
mock: z.array(classifiedPathSchema),
|
|
26
|
+
router: z.array(classifiedPathSchema),
|
|
27
|
+
hooks: z.array(classifiedPathSchema),
|
|
28
|
+
utils: z.array(classifiedPathSchema),
|
|
29
|
+
components: z.array(classifiedPathSchema),
|
|
30
|
+
other: z.array(classifiedPathSchema),
|
|
31
|
+
})
|
|
32
|
+
.strict(),
|
|
33
|
+
theme: z.array(classifiedPathSchema),
|
|
34
|
+
component: z.array(classifiedPathSchema),
|
|
35
|
+
uiOther: z.array(classifiedPathSchema),
|
|
36
|
+
advisoryOther: z.array(classifiedPathSchema),
|
|
37
|
+
})
|
|
38
|
+
.strict();
|
|
39
|
+
export const frontendDesignEvidenceSchema = z
|
|
40
|
+
.object({
|
|
41
|
+
normativePaths: z.array(z.string()),
|
|
42
|
+
advisoryPaths: z.array(z.string()),
|
|
43
|
+
conflicts: z.array(z.string()),
|
|
44
|
+
classified: frontendDesignClassifiedSchema,
|
|
45
|
+
})
|
|
46
|
+
.strict()
|
|
47
|
+
.superRefine((evidence, ctx) => {
|
|
48
|
+
const classified = [
|
|
49
|
+
...evidence.classified.schemas,
|
|
50
|
+
...evidence.classified.codeTemplate,
|
|
51
|
+
...evidence.classified.rule.api,
|
|
52
|
+
...evidence.classified.rule.mock,
|
|
53
|
+
...evidence.classified.rule.router,
|
|
54
|
+
...evidence.classified.rule.hooks,
|
|
55
|
+
...evidence.classified.rule.utils,
|
|
56
|
+
...evidence.classified.rule.components,
|
|
57
|
+
...evidence.classified.rule.other,
|
|
58
|
+
...evidence.classified.theme,
|
|
59
|
+
...evidence.classified.component,
|
|
60
|
+
...evidence.classified.uiOther,
|
|
61
|
+
...evidence.classified.advisoryOther,
|
|
62
|
+
];
|
|
63
|
+
const classifiedSet = new Set(classified);
|
|
64
|
+
const normativeSet = new Set(evidence.normativePaths);
|
|
65
|
+
const onlyClassified = [...classifiedSet].filter((candidate) => !normativeSet.has(candidate));
|
|
66
|
+
const onlyNormative = [...normativeSet].filter((candidate) => !classifiedSet.has(candidate));
|
|
67
|
+
if (onlyClassified.length > 0 || onlyNormative.length > 0) {
|
|
68
|
+
ctx.addIssue({
|
|
69
|
+
code: z.ZodIssueCode.custom,
|
|
70
|
+
message: `classified must partition normativePaths exactly; extra in classified: ${onlyClassified.join(", ") || "(none)"}; missing from classified: ${onlyNormative.join(", ") || "(none)"}`,
|
|
71
|
+
path: ["classified"],
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
});
|
|
9
75
|
async function exists(filePath) {
|
|
10
76
|
try {
|
|
11
77
|
await stat(filePath);
|
|
@@ -15,39 +81,68 @@ async function exists(filePath) {
|
|
|
15
81
|
return false;
|
|
16
82
|
}
|
|
17
83
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Resolve the governance subtree root from harness.json.governanceRoot.
|
|
86
|
+
* Falls back to DEFAULT_OPENSPEC_GOVERNANCE_ROOT when the manifest is missing,
|
|
87
|
+
* unreadable, malformed, or lacks a string governanceRoot. This intentionally
|
|
88
|
+
* does NOT apply init.ts's "docs" -> default migration (init-only behavior).
|
|
89
|
+
*/
|
|
90
|
+
export async function resolveOpenspecGovernanceRoot(repoRoot) {
|
|
91
|
+
const manifest = await readJson(path.join(repoRoot, "harness.json"));
|
|
92
|
+
if (manifest && typeof manifest === "object") {
|
|
93
|
+
const candidate = manifest.governanceRoot;
|
|
94
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
95
|
+
return candidate.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
96
|
+
}
|
|
24
97
|
}
|
|
98
|
+
return DEFAULT_OPENSPEC_GOVERNANCE_ROOT;
|
|
25
99
|
}
|
|
26
|
-
function
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
100
|
+
export async function listOpenspecDiscovery(repoRoot, options = {}) {
|
|
101
|
+
const governanceRoot = options.governanceRootOverride ??
|
|
102
|
+
(await resolveOpenspecGovernanceRoot(repoRoot));
|
|
103
|
+
const extraExcludedPrefixes = (options.extraExcludedPrefixes ?? [])
|
|
104
|
+
.map((prefix) => prefix.replace(/\\/g, "/").replace(/\/+$/, ""))
|
|
105
|
+
.filter(Boolean);
|
|
106
|
+
const collected = [];
|
|
107
|
+
let excludedGovernanceCount = 0;
|
|
108
|
+
const isExcluded = (rel) => {
|
|
109
|
+
if (isGovernanceSubtreePath(rel, governanceRoot))
|
|
110
|
+
return true;
|
|
111
|
+
return extraExcludedPrefixes.some((prefix) => rel === prefix || rel.startsWith(prefix + "/"));
|
|
31
112
|
};
|
|
32
|
-
}
|
|
33
|
-
function hasDep(deps, name) {
|
|
34
|
-
return Object.hasOwn(deps, name);
|
|
35
|
-
}
|
|
36
|
-
async function listOpenspec(repoRoot) {
|
|
37
|
-
const out = [];
|
|
38
113
|
for (const specDir of OPENSPEC_SPEC_DIRS) {
|
|
39
114
|
const root = path.join(repoRoot, specDir);
|
|
40
115
|
if (!(await exists(root)))
|
|
41
116
|
continue;
|
|
42
117
|
await walk(root, 0);
|
|
43
118
|
}
|
|
44
|
-
|
|
119
|
+
// openspec/schemas/** and openspec/project-specs/** sort first; remaining
|
|
120
|
+
// ai_workspace/** business files follow. Dedupe preserves discovery order.
|
|
121
|
+
const unique = [...new Set(collected)];
|
|
122
|
+
const openspecPaths = unique
|
|
123
|
+
.filter((candidate) => candidate.startsWith("openspec/"))
|
|
124
|
+
.sort();
|
|
125
|
+
const aiWorkspacePaths = unique
|
|
126
|
+
.filter((candidate) => candidate.startsWith("ai_workspace/"))
|
|
127
|
+
.sort();
|
|
128
|
+
const deduped = [...openspecPaths, ...aiWorkspacePaths];
|
|
129
|
+
const truncated = deduped.length > MAX_OPENSPEC_CANDIDATE_PATHS;
|
|
130
|
+
const paths = truncated
|
|
131
|
+
? deduped.slice(0, MAX_OPENSPEC_CANDIDATE_PATHS)
|
|
132
|
+
: deduped;
|
|
133
|
+
return {
|
|
134
|
+
paths,
|
|
135
|
+
governanceRoot,
|
|
136
|
+
excludedGovernanceCount,
|
|
137
|
+
truncated,
|
|
138
|
+
maxCandidates: MAX_OPENSPEC_CANDIDATE_PATHS,
|
|
139
|
+
};
|
|
45
140
|
async function walk(dir, depth) {
|
|
46
141
|
if (depth > 4)
|
|
47
142
|
return;
|
|
48
143
|
let entries = [];
|
|
49
144
|
try {
|
|
50
|
-
entries = await readdir(dir);
|
|
145
|
+
entries = (await readdir(dir)).sort();
|
|
51
146
|
}
|
|
52
147
|
catch {
|
|
53
148
|
return;
|
|
@@ -59,7 +154,13 @@ async function listOpenspec(repoRoot) {
|
|
|
59
154
|
if (info.isDirectory())
|
|
60
155
|
await walk(full, depth + 1);
|
|
61
156
|
else if (OPENSPEC_SPEC_EXT_RE.test(name)) {
|
|
62
|
-
|
|
157
|
+
const rel = path.relative(repoRoot, full).replace(/\\/g, "/");
|
|
158
|
+
if (isExcluded(rel)) {
|
|
159
|
+
excludedGovernanceCount++;
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
collected.push(rel);
|
|
163
|
+
}
|
|
63
164
|
}
|
|
64
165
|
}
|
|
65
166
|
catch {
|
|
@@ -68,6 +169,164 @@ async function listOpenspec(repoRoot) {
|
|
|
68
169
|
}
|
|
69
170
|
}
|
|
70
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Count spec-extension files under a governance subtree that candidate
|
|
174
|
+
* discovery would exclude. Bounded, read-only diagnostic helper used by the
|
|
175
|
+
* prewrite gate to reproduce exclusion stats at runtime.
|
|
176
|
+
*/
|
|
177
|
+
export async function countExcludedGovernanceSpecFiles(repoRoot, governanceRoot) {
|
|
178
|
+
const root = path.join(repoRoot, governanceRoot);
|
|
179
|
+
if (!(await exists(root)))
|
|
180
|
+
return 0;
|
|
181
|
+
let count = 0;
|
|
182
|
+
await walkGovernance(root, 0);
|
|
183
|
+
return count;
|
|
184
|
+
async function walkGovernance(dir, depth) {
|
|
185
|
+
if (depth > 4)
|
|
186
|
+
return;
|
|
187
|
+
let entries = [];
|
|
188
|
+
try {
|
|
189
|
+
entries = (await readdir(dir)).sort();
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
for (const name of entries) {
|
|
195
|
+
const full = path.join(dir, name);
|
|
196
|
+
try {
|
|
197
|
+
const info = await stat(full);
|
|
198
|
+
if (info.isDirectory())
|
|
199
|
+
await walkGovernance(full, depth + 1);
|
|
200
|
+
else if (OPENSPEC_SPEC_EXT_RE.test(name))
|
|
201
|
+
count++;
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
// skip
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async function readJson(filePath) {
|
|
210
|
+
try {
|
|
211
|
+
return JSON.parse(await readFile(filePath, "utf8"));
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function allDeps(pkg) {
|
|
218
|
+
return {
|
|
219
|
+
...(pkg.dependencies ?? {}),
|
|
220
|
+
...(pkg.devDependencies ?? {}),
|
|
221
|
+
...(pkg.peerDependencies ?? {}),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function hasDep(deps, name) {
|
|
225
|
+
return Object.hasOwn(deps, name);
|
|
226
|
+
}
|
|
227
|
+
function emptyClassified() {
|
|
228
|
+
return {
|
|
229
|
+
schemas: [],
|
|
230
|
+
codeTemplate: [],
|
|
231
|
+
rule: { api: [], mock: [], router: [], hooks: [], utils: [], components: [], other: [] },
|
|
232
|
+
theme: [],
|
|
233
|
+
component: [],
|
|
234
|
+
uiOther: [],
|
|
235
|
+
advisoryOther: [],
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Classify discovered openspec paths by generic directory convention. Only
|
|
240
|
+
* `isOpenspecSpecFilePath` paths enter a bucket; the classifier never invents
|
|
241
|
+
* project-specific file names. Basename stems use word-boundary matching so
|
|
242
|
+
* e.g. `*api*` (case-insensitive) maps to `rule.api` without hardcoding names.
|
|
243
|
+
*/
|
|
244
|
+
function classifyOpenspecPaths(paths) {
|
|
245
|
+
const classified = emptyClassified();
|
|
246
|
+
const ruleStem = (basename) => {
|
|
247
|
+
const lower = basename.toLowerCase();
|
|
248
|
+
const stems = [
|
|
249
|
+
["api", /\bapi\b/],
|
|
250
|
+
["mock", /\bmock\b/],
|
|
251
|
+
["router", /\brouter\b/],
|
|
252
|
+
["hooks", /\bhooks?\b/],
|
|
253
|
+
["utils", /\butils?\b/],
|
|
254
|
+
["components", /\bcomponents?\b/],
|
|
255
|
+
];
|
|
256
|
+
for (const [key, re] of stems) {
|
|
257
|
+
if (re.test(lower))
|
|
258
|
+
return key;
|
|
259
|
+
}
|
|
260
|
+
return "other";
|
|
261
|
+
};
|
|
262
|
+
for (const candidate of paths) {
|
|
263
|
+
if (!isOpenspecSpecFilePath(candidate))
|
|
264
|
+
continue;
|
|
265
|
+
const segments = candidate.split("/");
|
|
266
|
+
const basename = segments[segments.length - 1] ?? "";
|
|
267
|
+
if (candidate.startsWith("openspec/schemas/")) {
|
|
268
|
+
classified.schemas.push(candidate);
|
|
269
|
+
}
|
|
270
|
+
else if (candidate.startsWith("openspec/project-specs/templates/")) {
|
|
271
|
+
classified.codeTemplate.push(candidate);
|
|
272
|
+
}
|
|
273
|
+
else if (candidate.startsWith("openspec/project-specs/rules/")) {
|
|
274
|
+
classified.rule[ruleStem(basename)].push(candidate);
|
|
275
|
+
}
|
|
276
|
+
else if (candidate.startsWith("openspec/project-specs/ui/")) {
|
|
277
|
+
const lower = basename.toLowerCase();
|
|
278
|
+
if (/\btheme\b/.test(lower))
|
|
279
|
+
classified.theme.push(candidate);
|
|
280
|
+
else if (/\bcomponents?\b/.test(lower))
|
|
281
|
+
classified.component.push(candidate);
|
|
282
|
+
else
|
|
283
|
+
classified.uiOther.push(candidate);
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
classified.advisoryOther.push(candidate);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const sortBucket = (bucket) => bucket.sort();
|
|
290
|
+
classified.schemas = sortBucket(classified.schemas);
|
|
291
|
+
classified.codeTemplate = sortBucket(classified.codeTemplate);
|
|
292
|
+
for (const key of [
|
|
293
|
+
"api",
|
|
294
|
+
"mock",
|
|
295
|
+
"router",
|
|
296
|
+
"hooks",
|
|
297
|
+
"utils",
|
|
298
|
+
"components",
|
|
299
|
+
"other",
|
|
300
|
+
]) {
|
|
301
|
+
classified.rule[key] = sortBucket(classified.rule[key]);
|
|
302
|
+
}
|
|
303
|
+
classified.theme = sortBucket(classified.theme);
|
|
304
|
+
classified.component = sortBucket(classified.component);
|
|
305
|
+
classified.uiOther = sortBucket(classified.uiOther);
|
|
306
|
+
classified.advisoryOther = sortBucket(classified.advisoryOther);
|
|
307
|
+
return classified;
|
|
308
|
+
}
|
|
309
|
+
function buildDesignEvidence(openspec) {
|
|
310
|
+
// Preserve discovery priority order (openspec first, then ai_workspace)
|
|
311
|
+
// instead of re-sorting; the exact-partition schema identity is set-based.
|
|
312
|
+
const normativePaths = [...new Set(openspec)];
|
|
313
|
+
return frontendDesignEvidenceSchema.parse({
|
|
314
|
+
normativePaths,
|
|
315
|
+
advisoryPaths: [],
|
|
316
|
+
conflicts: [],
|
|
317
|
+
classified: classifyOpenspecPaths(normativePaths),
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
function toOpenspecDiscovery(discovery, designEvidence) {
|
|
321
|
+
return {
|
|
322
|
+
source: "auto-discovered",
|
|
323
|
+
governanceRoot: discovery.governanceRoot,
|
|
324
|
+
candidateCount: designEvidence.normativePaths.length,
|
|
325
|
+
excludedGovernanceCount: discovery.excludedGovernanceCount,
|
|
326
|
+
truncated: discovery.truncated,
|
|
327
|
+
maxCandidates: discovery.maxCandidates,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
71
330
|
export function buildAdapterGuidance(capability) {
|
|
72
331
|
const lines = [
|
|
73
332
|
"## Frontend project capability (generation-time)",
|
|
@@ -108,7 +367,8 @@ export async function discoverFrontendProjectCapability(repoRoot) {
|
|
|
108
367
|
const pkgPath = path.join(repoRoot, "package.json");
|
|
109
368
|
const pkgRaw = await readJson(pkgPath);
|
|
110
369
|
if (!pkgRaw || typeof pkgRaw !== "object") {
|
|
111
|
-
const
|
|
370
|
+
const discovery = await listOpenspecDiscovery(repoRoot);
|
|
371
|
+
const designEvidence = buildDesignEvidence(discovery.paths);
|
|
112
372
|
const base = {
|
|
113
373
|
schemaVersion: 1,
|
|
114
374
|
framework: "unknown",
|
|
@@ -119,16 +379,13 @@ export async function discoverFrontendProjectCapability(repoRoot) {
|
|
|
119
379
|
testRunner: [],
|
|
120
380
|
mock: [],
|
|
121
381
|
a11y: { status: "unknown", tools: [], evidencePaths: [] },
|
|
122
|
-
evidencePaths:
|
|
382
|
+
evidencePaths: discovery.paths.slice(0, 5),
|
|
123
383
|
reasons: ["package.json missing or unreadable"],
|
|
124
|
-
designEvidence
|
|
125
|
-
|
|
126
|
-
advisoryPaths: [],
|
|
127
|
-
conflicts: [],
|
|
128
|
-
},
|
|
384
|
+
designEvidence,
|
|
385
|
+
openspecDiscovery: toOpenspecDiscovery(discovery, designEvidence),
|
|
129
386
|
};
|
|
130
|
-
if (
|
|
131
|
-
base.reasons.push(`openspec spec files discovered: ${
|
|
387
|
+
if (discovery.paths.length) {
|
|
388
|
+
base.reasons.push(`openspec spec files discovered: ${discovery.paths.slice(0, 5).join(", ")}`);
|
|
132
389
|
}
|
|
133
390
|
return { ...base, adapterGuidance: buildAdapterGuidance(base) };
|
|
134
391
|
}
|
|
@@ -283,14 +540,10 @@ export async function discoverFrontendProjectCapability(repoRoot) {
|
|
|
283
540
|
router = router ?? "next-router";
|
|
284
541
|
if (hasDep(deps, "vue-router"))
|
|
285
542
|
router = "vue-router";
|
|
286
|
-
const
|
|
287
|
-
const designEvidence =
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
conflicts: [],
|
|
291
|
-
};
|
|
292
|
-
if (openspec.length)
|
|
293
|
-
evidencePaths.push(...openspec.slice(0, 5));
|
|
543
|
+
const discovery = await listOpenspecDiscovery(repoRoot);
|
|
544
|
+
const designEvidence = buildDesignEvidence(discovery.paths);
|
|
545
|
+
if (discovery.paths.length)
|
|
546
|
+
evidencePaths.push(...discovery.paths.slice(0, 5));
|
|
294
547
|
const base = {
|
|
295
548
|
schemaVersion: 1,
|
|
296
549
|
framework,
|
|
@@ -311,6 +564,7 @@ export async function discoverFrontendProjectCapability(repoRoot) {
|
|
|
311
564
|
evidencePaths: [...new Set(evidencePaths)],
|
|
312
565
|
reasons,
|
|
313
566
|
designEvidence,
|
|
567
|
+
openspecDiscovery: toOpenspecDiscovery(discovery, designEvidence),
|
|
314
568
|
};
|
|
315
569
|
return { ...base, adapterGuidance: buildAdapterGuidance(base) };
|
|
316
570
|
}
|
|
@@ -92,36 +92,25 @@ function sortToolsStable(tools) {
|
|
|
92
92
|
});
|
|
93
93
|
}
|
|
94
94
|
/**
|
|
95
|
-
* Flush buffered consecutive tools into process items
|
|
96
|
-
*
|
|
97
|
-
*
|
|
95
|
+
* Flush buffered consecutive tools into process items (pi-web aligned:
|
|
96
|
+
* strict sequential flow, no synthetic grouping). Semantic operator tools
|
|
97
|
+
* stay as individual activity events; plain tools each render as their own
|
|
98
|
+
* card in stable server order (eventSeq first, createdAt fallback).
|
|
98
99
|
* Thinking/text/activity boundaries break the buffer upstream.
|
|
99
100
|
*/
|
|
100
101
|
function flushToolBuffer(processItems, buffer) {
|
|
101
102
|
if (buffer.length === 0)
|
|
102
103
|
return;
|
|
103
104
|
const sorted = sortToolsStable(buffer);
|
|
104
|
-
let plainGroup = [];
|
|
105
|
-
const flushPlain = () => {
|
|
106
|
-
if (plainGroup.length === 1) {
|
|
107
|
-
processItems.push({ kind: "tool", tool: plainGroup[0] });
|
|
108
|
-
}
|
|
109
|
-
else if (plainGroup.length > 1) {
|
|
110
|
-
processItems.push({ kind: "tool-group", tools: plainGroup });
|
|
111
|
-
}
|
|
112
|
-
plainGroup = [];
|
|
113
|
-
};
|
|
114
105
|
for (const tool of sorted) {
|
|
115
106
|
const activity = semanticActivityForTool(tool);
|
|
116
107
|
if (activity) {
|
|
117
|
-
flushPlain();
|
|
118
108
|
processItems.push({ kind: "activity", activity });
|
|
119
109
|
}
|
|
120
110
|
else {
|
|
121
|
-
|
|
111
|
+
processItems.push({ kind: "tool", tool });
|
|
122
112
|
}
|
|
123
113
|
}
|
|
124
|
-
flushPlain();
|
|
125
114
|
}
|
|
126
115
|
function emitProcessBlocks(blocks, group, processItems, usedToolIds) {
|
|
127
116
|
let buffer = [];
|
|
@@ -324,7 +313,7 @@ export function processGroupSummary(tools, options) {
|
|
|
324
313
|
parts.push("运行中");
|
|
325
314
|
return { count, errorCount, runningCount, stepCount, label: parts.join(" · ") };
|
|
326
315
|
}
|
|
327
|
-
/** Summary over rendered process items
|
|
316
|
+
/** Summary over rendered process items. */
|
|
328
317
|
export function summarizeProcessItems(items) {
|
|
329
318
|
const tools = [];
|
|
330
319
|
let thinkingCount = 0;
|
|
@@ -344,10 +333,8 @@ export function summarizeProcessItems(items) {
|
|
|
344
333
|
else if (item.activity.kind === "progress")
|
|
345
334
|
activityRunningCount += 1;
|
|
346
335
|
}
|
|
347
|
-
else if (item.kind === "tool")
|
|
348
|
-
tools.push(item.tool);
|
|
349
336
|
else
|
|
350
|
-
tools.push(
|
|
337
|
+
tools.push(item.tool);
|
|
351
338
|
}
|
|
352
339
|
return processGroupSummary(tools, {
|
|
353
340
|
thinkingCount,
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* harness.json executors.pi LOW/MED/HIGH 模型矩阵的读写与校验。
|
|
3
|
+
*
|
|
4
|
+
* 该模块只服务于 Console「模型与认证」页的模型矩阵编辑器:
|
|
5
|
+
* - `validatePiModelMatrixTiers`:写入前拒绝空串 / default 哨兵 / 非字符串 /
|
|
6
|
+
* 畸形 provider/model(如 provider/、/model、a//b)。
|
|
7
|
+
* - `readPiModelMatrixProjection`:读全文档并投影三档的 provider/model 或裸
|
|
8
|
+
* model id(供 readiness 结构化投影 piModelMatrix 使用)。
|
|
9
|
+
* - `writePiModelMatrix`:读全文档 → 校验 → 原子写 tmp + fsync + rename,
|
|
10
|
+
* 保留 $schema 与其余所有字段。
|
|
11
|
+
*
|
|
12
|
+
* 本模块在 worker/console(Node 侧),不得被浏览器静态资源直接 import
|
|
13
|
+
* (依赖 node:fs / node:crypto / node:path)。
|
|
14
|
+
*/
|
|
15
|
+
import { randomBytes } from "node:crypto";
|
|
16
|
+
import { open, readFile, rename, writeFile } from "node:fs/promises";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { normalizeExecutorTierValue } from "../../governance/manifest-types.js";
|
|
19
|
+
export const PI_MODEL_TIERS = ["LOW", "MED", "HIGH"];
|
|
20
|
+
/** 校验单档字符串值,返回错误文案;合法返回 undefined。 */
|
|
21
|
+
export function piModelTierValueError(value) {
|
|
22
|
+
if (typeof value !== "string")
|
|
23
|
+
return "模型值必须是字符串";
|
|
24
|
+
if (value.length === 0)
|
|
25
|
+
return "模型值不能为空";
|
|
26
|
+
if (value.trim() !== value)
|
|
27
|
+
return "模型值不能包含首尾空白";
|
|
28
|
+
if (value === "default")
|
|
29
|
+
return "default 是保留哨兵,不能作为模型值";
|
|
30
|
+
// 比 normalizeExecutorTierValue 更严格:拒绝 provider/、/model、a//b。
|
|
31
|
+
if (value.startsWith("/") || value.endsWith("/") || value.includes("//")) {
|
|
32
|
+
return "provider/model 格式非法:不允许 /model、provider/ 或 a//b";
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 写入前校验三档字符串值。任何一档非法即整体拒绝,保证不落盘。
|
|
38
|
+
*/
|
|
39
|
+
export function validatePiModelMatrixTiers(input) {
|
|
40
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
code: "INVALID_INPUT",
|
|
44
|
+
message: "模型矩阵必须是 { LOW, MED, HIGH } 对象",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const record = input;
|
|
48
|
+
const tiers = {};
|
|
49
|
+
for (const tier of PI_MODEL_TIERS) {
|
|
50
|
+
const raw = record[tier];
|
|
51
|
+
const error = piModelTierValueError(raw);
|
|
52
|
+
if (error) {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
code: "INVALID_INPUT",
|
|
56
|
+
message: `${tier}: ${error}`,
|
|
57
|
+
tier,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
tiers[tier] = raw;
|
|
61
|
+
}
|
|
62
|
+
return { ok: true, tiers };
|
|
63
|
+
}
|
|
64
|
+
async function readHarnessJson(repoRoot) {
|
|
65
|
+
try {
|
|
66
|
+
const raw = await readFile(path.join(repoRoot, "harness.json"), "utf8");
|
|
67
|
+
const parsed = JSON.parse(raw);
|
|
68
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
return parsed;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 投影 harness.json executors.pi 三档(provider/model 或裸 model id)。
|
|
79
|
+
* 缺文件 / 非法 JSON / 未配置档位均优雅降级为缺省,绝不抛错。
|
|
80
|
+
*/
|
|
81
|
+
export async function readPiModelMatrixProjection(repoRoot) {
|
|
82
|
+
const manifest = await readHarnessJson(repoRoot);
|
|
83
|
+
const pi = manifest?.executors?.pi;
|
|
84
|
+
const projection = {};
|
|
85
|
+
const options = new Set();
|
|
86
|
+
if (!pi || typeof pi !== "object")
|
|
87
|
+
return { ...projection, options: [] };
|
|
88
|
+
const configuredOptions = Array.isArray(pi.modelOptions)
|
|
89
|
+
? pi.modelOptions.filter((item) => typeof item === "string" &&
|
|
90
|
+
item.trim().length > 0 &&
|
|
91
|
+
item.trim() === item)
|
|
92
|
+
: [];
|
|
93
|
+
for (const option of configuredOptions)
|
|
94
|
+
options.add(option);
|
|
95
|
+
for (const tier of PI_MODEL_TIERS) {
|
|
96
|
+
const selection = normalizeExecutorTierValue(pi[tier]);
|
|
97
|
+
if (!selection)
|
|
98
|
+
continue;
|
|
99
|
+
projection[tier] = {
|
|
100
|
+
model: selection.model,
|
|
101
|
+
...(selection.provider ? { provider: selection.provider } : {}),
|
|
102
|
+
value: selection.provider
|
|
103
|
+
? `${selection.provider}/${selection.model}`
|
|
104
|
+
: selection.model,
|
|
105
|
+
};
|
|
106
|
+
options.add(projection[tier].value);
|
|
107
|
+
}
|
|
108
|
+
return { ...projection, options: [...options] };
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* 原子更新 harness.json executors.pi.LOW/MED/HIGH:
|
|
112
|
+
* 读全文档 → 校验 → 保留其余字段 → 写 tmp + fsync + rename。
|
|
113
|
+
*/
|
|
114
|
+
export async function writePiModelMatrix(repoRoot, tiers) {
|
|
115
|
+
const validation = validatePiModelMatrixTiers(tiers);
|
|
116
|
+
if (!validation.ok) {
|
|
117
|
+
throw Object.assign(new Error(validation.message), {
|
|
118
|
+
code: validation.code,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const file = path.join(repoRoot, "harness.json");
|
|
122
|
+
const manifest = await readHarnessJson(repoRoot);
|
|
123
|
+
if (!manifest) {
|
|
124
|
+
throw Object.assign(new Error("harness.json 不存在或不是合法 JSON"), {
|
|
125
|
+
code: "HARNESS_NOT_FOUND",
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
const executors = (manifest.executors ?? {});
|
|
129
|
+
const pi = (executors.pi ?? {});
|
|
130
|
+
const nextPi = { ...pi };
|
|
131
|
+
for (const tier of PI_MODEL_TIERS) {
|
|
132
|
+
nextPi[tier] = validation.tiers[tier];
|
|
133
|
+
}
|
|
134
|
+
const nextManifest = { ...manifest, executors: { ...executors, pi: nextPi } };
|
|
135
|
+
const body = `${JSON.stringify(nextManifest, null, 2)}\n`;
|
|
136
|
+
const tmp = path.join(repoRoot, `.harness.${process.pid}.${randomBytes(4).toString("hex")}.tmp`);
|
|
137
|
+
await writeFile(tmp, body, { encoding: "utf8" });
|
|
138
|
+
const handle = await open(tmp, "r+");
|
|
139
|
+
try {
|
|
140
|
+
await handle.sync();
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
await handle.close();
|
|
144
|
+
}
|
|
145
|
+
await rename(tmp, file);
|
|
146
|
+
}
|
|
@@ -11,6 +11,7 @@ import { resolveLatestDagRunIdForTask } from "./resolve-dag-run-for-task.js";
|
|
|
11
11
|
import { repoFingerprintV1 } from "./repo-fingerprint.js";
|
|
12
12
|
import { scheduleOperation, } from "./operation-runner.js";
|
|
13
13
|
import { readinessGateFailure, } from "./pi-readiness.js";
|
|
14
|
+
import { validatePiModelMatrixTiers, writePiModelMatrix, } from "./harness-pi-model-matrix.js";
|
|
14
15
|
import { isAssessmentFreshForDraft } from "./interview/assessment.js";
|
|
15
16
|
import { applyGrillMeAnswer, draftSummary, emptyDraft, isDraftStructurallyComplete, nextGrillMeQuestion, } from "./interview/grill-me.js";
|
|
16
17
|
import { toCanonicalDraftForCli, } from "./draft-store.js";
|
|
@@ -2483,6 +2484,54 @@ export async function dispatchOperatorAction(ctx, req) {
|
|
|
2483
2484
|
}),
|
|
2484
2485
|
};
|
|
2485
2486
|
}
|
|
2487
|
+
case "savePiModelMatrix": {
|
|
2488
|
+
// 「模型与认证」页模型矩阵编辑器:三档字符串值经校验后原子写入
|
|
2489
|
+
// harness.json executors.pi,并自动重测 readiness。写路径本身由 HTTP
|
|
2490
|
+
// 层 evaluateMutationGate 拦截(cookie + confirmation 头 + origin)。
|
|
2491
|
+
const matrixInput = p.matrix ?? p;
|
|
2492
|
+
const validation = validatePiModelMatrixTiers(matrixInput);
|
|
2493
|
+
if (!validation.ok) {
|
|
2494
|
+
return {
|
|
2495
|
+
kind: "error",
|
|
2496
|
+
status: 400,
|
|
2497
|
+
body: operatorFailed({
|
|
2498
|
+
command: action,
|
|
2499
|
+
outcome: "invalid",
|
|
2500
|
+
code: validation.code,
|
|
2501
|
+
message: validation.message,
|
|
2502
|
+
...(validation.tier ? { details: { tier: validation.tier } } : {}),
|
|
2503
|
+
}),
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
try {
|
|
2507
|
+
await writePiModelMatrix(ctx.repoRoot, validation.tiers);
|
|
2508
|
+
}
|
|
2509
|
+
catch (error) {
|
|
2510
|
+
return {
|
|
2511
|
+
kind: "error",
|
|
2512
|
+
status: 500,
|
|
2513
|
+
body: operatorFailed({
|
|
2514
|
+
command: action,
|
|
2515
|
+
outcome: "blocked",
|
|
2516
|
+
code: "PI_MODEL_MATRIX_WRITE_FAILED",
|
|
2517
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2518
|
+
}),
|
|
2519
|
+
};
|
|
2520
|
+
}
|
|
2521
|
+
// 保存成功自动重测 readiness(复用 probePiReadiness),让页面立即
|
|
2522
|
+
// 展示新的 modelsAvailable / modelsMessage / piModelMatrix(AC-4)。
|
|
2523
|
+
const report = ctx.refreshReadiness
|
|
2524
|
+
? await ctx.refreshReadiness()
|
|
2525
|
+
: ctx.getReadiness?.() ?? ctx.readiness;
|
|
2526
|
+
return {
|
|
2527
|
+
kind: "sync",
|
|
2528
|
+
status: 200,
|
|
2529
|
+
body: operatorSucceeded("savePiModelMatrix", {
|
|
2530
|
+
tiers: validation.tiers,
|
|
2531
|
+
readiness: report,
|
|
2532
|
+
}),
|
|
2533
|
+
};
|
|
2534
|
+
}
|
|
2486
2535
|
default: {
|
|
2487
2536
|
// M5 official-command coverage uses one generic tail for newly registered
|
|
2488
2537
|
// CLI actions. It remains inside dispatchOperatorAction (the sole dispatch
|