create-yss-spec 3.4.9 → 3.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/package.json +1 -1
- package/src/template/attach-planner.js +5 -0
- package/src/template/sync-planner.js +5 -0
- package/template/.agents/skills/.strategic-design-skills-manifest.json +1 -1
- package/template/.agents/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
- package/template/.agents/skills/yss-openapi-draft-review/SKILL.md +2 -0
- package/template/.agents/skills/yss-openapi-governance/SKILL.md +4 -0
- package/template/.agents/skills/yss-product-lifecycle/SKILL.md +11 -5
- package/template/.agents/skills/yss-product-lifecycle/references/orchestration-contract.yaml +63 -1
- package/template/.agents/skills/yss-product-lifecycle/references/orchestration.md +2 -2
- package/template/.agents/skills/yss-prototype-stage/SKILL.md +2 -0
- package/template/.agents/skills/yss-prototype-stage/scripts/visual-baseline-contract.mjs +2 -2
- package/template/.agents/skills/yss-stage-decision/SKILL.md +2 -0
- package/template/.agents/skills/yss-tactical-design/SKILL.md +2 -0
- package/template/.agents/skills/yss-technical-design/SKILL.md +2 -0
- package/template/.agents/skills/yss-technical-design/scripts/engineering-design.mjs +1 -1
- package/template/.agents/skills/yss-technical-design/scripts/validate-technical-design.mjs +11 -8
- package/template/.codex/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
- package/template/.codex/skills/yss-openapi-draft-review/SKILL.md +2 -0
- package/template/.codex/skills/yss-openapi-governance/SKILL.md +4 -0
- package/template/.codex/skills/yss-product-lifecycle/SKILL.md +11 -5
- package/template/.codex/skills/yss-product-lifecycle/references/orchestration-contract.yaml +63 -1
- package/template/.codex/skills/yss-product-lifecycle/references/orchestration.md +2 -2
- package/template/.codex/skills/yss-prototype-stage/SKILL.md +2 -0
- package/template/.codex/skills/yss-prototype-stage/scripts/visual-baseline-contract.mjs +2 -2
- package/template/.codex/skills/yss-stage-decision/SKILL.md +2 -0
- package/template/.codex/skills/yss-tactical-design/SKILL.md +2 -0
- package/template/.codex/skills/yss-technical-design/SKILL.md +2 -0
- package/template/.codex/skills/yss-technical-design/scripts/engineering-design.mjs +1 -1
- package/template/.codex/skills/yss-technical-design/scripts/validate-technical-design.mjs +11 -8
- package/template/.cursor/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
- package/template/.cursor/skills/yss-openapi-draft-review/SKILL.md +2 -0
- package/template/.cursor/skills/yss-openapi-governance/SKILL.md +4 -0
- package/template/.cursor/skills/yss-product-lifecycle/SKILL.md +11 -5
- package/template/.cursor/skills/yss-product-lifecycle/references/orchestration-contract.yaml +63 -1
- package/template/.cursor/skills/yss-product-lifecycle/references/orchestration.md +2 -2
- package/template/.cursor/skills/yss-prototype-stage/SKILL.md +2 -0
- package/template/.cursor/skills/yss-prototype-stage/scripts/visual-baseline-contract.mjs +2 -2
- package/template/.cursor/skills/yss-stage-decision/SKILL.md +2 -0
- package/template/.cursor/skills/yss-tactical-design/SKILL.md +2 -0
- package/template/.cursor/skills/yss-technical-design/SKILL.md +2 -0
- package/template/.cursor/skills/yss-technical-design/scripts/engineering-design.mjs +1 -1
- package/template/.cursor/skills/yss-technical-design/scripts/validate-technical-design.mjs +11 -8
- package/template/.pi/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
- package/template/.pi/skills/yss-openapi-draft-review/SKILL.md +2 -0
- package/template/.pi/skills/yss-openapi-governance/SKILL.md +4 -0
- package/template/.pi/skills/yss-product-lifecycle/SKILL.md +11 -5
- package/template/.pi/skills/yss-product-lifecycle/references/orchestration-contract.yaml +63 -1
- package/template/.pi/skills/yss-product-lifecycle/references/orchestration.md +2 -2
- package/template/.pi/skills/yss-prototype-stage/SKILL.md +2 -0
- package/template/.pi/skills/yss-prototype-stage/scripts/visual-baseline-contract.mjs +2 -2
- package/template/.pi/skills/yss-stage-decision/SKILL.md +2 -0
- package/template/.pi/skills/yss-tactical-design/SKILL.md +2 -0
- package/template/.pi/skills/yss-technical-design/SKILL.md +2 -0
- package/template/.pi/skills/yss-technical-design/scripts/engineering-design.mjs +1 -1
- package/template/.pi/skills/yss-technical-design/scripts/validate-technical-design.mjs +11 -8
- package/template/AGENTS.md +2 -0
- package/template/CONTEXT.md +2 -1
- package/template/README.md +1 -0
- package/template/docs/agents/issue-tracker.md +7 -0
- package/template/docs/process/contract-reading.md +46 -0
- package/template/docs/process/lifecycle-artifact-map.md +2 -2
- package/template/docs/process/lifecycle-registry-baseline.json +2 -1
- package/template/docs/process/lifecycle-registry.yaml +10 -4
- package/template/docs/process/schemas/api-contract-decision-v2.schema.json +198 -0
- package/template/docs/process/schemas/digital-human-task-package.schema.json +8 -0
- package/template/docs/process/schemas/lifecycle-checkpoint.schema.json +259 -0
- package/template/docs/process/schemas/stage-tracking.schema.json +259 -0
- package/template/docs/process/stage-tracking.md +62 -0
- package/template/docs/process/strategic-handoff-package.md +8 -0
- package/template/docs/user-guide//347/224/250/346/210/267/346/211/213/345/206/214.md +2 -0
- package/template/docs/user-guide//347/224/250/346/210/267/346/211/213/345/206/214/347/264/242/345/274/225.md +1 -0
- package/template/docs/user-guide//351/230/266/346/256/265/346/216/245/345/205/245/344/270/216/344/272/247/347/211/251/345/244/215/347/224/250/344/275/277/347/224/250/346/211/213/345/206/214.md +243 -0
- package/template/scripts/api-contract-decision +16 -0
- package/template/scripts/complete-backend-delivery +17 -0
- package/template/scripts/contract +13 -0
- package/template/scripts/dispatch-slice-task +13 -0
- package/template/scripts/lib/api-contract-decision.mjs +45 -4
- package/template/scripts/lib/approval-record.mjs +1 -1
- package/template/scripts/lib/approved-execution-context.mjs +35 -7
- package/template/scripts/lib/backend-architecture.mjs +2 -2
- package/template/scripts/lib/backend-delivery-terminal.mjs +51 -0
- package/template/scripts/lib/backend-platform-provenance.mjs +1 -1
- package/template/scripts/lib/backend-platform.mjs +1 -1
- package/template/scripts/lib/backend-scaffold-prerequisites.mjs +1 -1
- package/template/scripts/lib/context-contract.mjs +1 -1
- package/template/scripts/lib/context-reconciliation.mjs +1 -1
- package/template/scripts/lib/contract-source.mjs +22 -0
- package/template/scripts/lib/contract-views.mjs +109 -0
- package/template/scripts/lib/delivery-preflight.mjs +3 -2
- package/template/scripts/lib/digital-human-roles.mjs +1 -1
- package/template/scripts/lib/existing-backend-architecture.mjs +4 -3
- package/template/scripts/lib/existing-ui-baseline.mjs +4 -2
- package/template/scripts/lib/frontend-delivery-boundary.mjs +1 -1
- package/template/scripts/lib/frontend-delivery.mjs +1 -1
- package/template/scripts/lib/harness-execution-scope.mjs +15 -1
- package/template/scripts/lib/implementation-contract-compiler.mjs +4 -4
- package/template/scripts/lib/json-schema.mjs +19 -4
- package/template/scripts/lib/lifecycle-context-query.mjs +5 -1
- package/template/scripts/lib/lifecycle-controls.mjs +1 -1
- package/template/scripts/lib/lifecycle-execution-scope.mjs +92 -0
- package/template/scripts/lib/lifecycle-registry.mjs +2 -1
- package/template/scripts/lib/lifecycle-transition.mjs +21 -3
- package/template/scripts/lib/openapi-draft-validation.mjs +2 -1
- package/template/scripts/lib/skill-registry.mjs +1 -1
- package/template/scripts/lib/slice-contract-preparation.mjs +16 -14
- package/template/scripts/lib/slice-contract-views.mjs +1 -1
- package/template/scripts/lib/slice-contract.mjs +18 -3
- package/template/scripts/lib/slice-execution-preflight.mjs +7 -3
- package/template/scripts/lib/slice-repositories.mjs +1 -1
- package/template/scripts/lib/slice-task-package.mjs +3 -2
- package/template/scripts/lib/stage-tracking-migration.mjs +176 -0
- package/template/scripts/lib/stage-tracking.mjs +184 -0
- package/template/scripts/lib/strategic-handoff-consumption.mjs +5 -4
- package/template/scripts/lib/strategic-handoff-io.mjs +5 -2
- package/template/scripts/lib/strategic-handoff-routing.mjs +31 -0
- package/template/scripts/lib/strategic-handoff.mjs +4 -2
- package/template/scripts/lib/task-package.mjs +2 -0
- package/template/scripts/lib/technical-design-boundary.mjs +4 -2
- package/template/scripts/lib/ui-baseline.mjs +1 -1
- package/template/scripts/lib/user-decision.mjs +1 -1
- package/template/scripts/lib/validation-phase.mjs +72 -0
- package/template/scripts/stage-tracking +25 -0
- package/template/scripts/strategic-consumer-entry +47 -0
- package/template/scripts/verify-context-reconciliation +4 -3
- package/template/scripts/verify-lifecycle-checkpoint +10 -0
- package/template/skills-lock.json +13 -13
- package/template.snapshot.json +4 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import fs from '
|
|
1
|
+
import fs from './validation-phase.mjs';
|
|
2
2
|
import { safe, hash, digest } from './strategic-handoff-io.mjs';
|
|
3
3
|
import { selectSliceWorkUnit, readSliceContract, readSliceSources, sliceAcceptanceText, parseSliceYaml, sourceSliceContract } from './slice-contract.mjs';
|
|
4
4
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import {validateExistingUiBaseline} from './existing-ui-baseline.mjs';
|
|
2
2
|
import {sliceRepositories} from './slice-repositories.mjs';
|
|
3
3
|
import { sliceCheckApplicability } from './slice-applicability.mjs';
|
|
4
|
-
import fs from '
|
|
4
|
+
import fs, {validationMemo} from './validation-phase.mjs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { parseDocument } from '../vendor/yaml.mjs';
|
|
7
7
|
import { safe, hash, digest, schema } from './strategic-handoff-io.mjs';
|
|
8
8
|
|
|
9
|
+
const parseCache=Symbol('slice-yaml');
|
|
9
10
|
const originals = new WeakMap();
|
|
10
11
|
const sourceSnapshots = new WeakMap();
|
|
11
12
|
const nonempty = value => typeof value === 'string' && value.trim();
|
|
@@ -13,7 +14,8 @@ const unique = values => [...new Set(values)];
|
|
|
13
14
|
const fail = message => { throw new TypeError(`slice-contract-invalid: ${message}`); };
|
|
14
15
|
const requireThat = (ok, message) => { if (!ok) fail(message); };
|
|
15
16
|
export const sourceSliceContract = value => originals.get(value) || value?.slice_contract || value;
|
|
16
|
-
export function parseSliceYaml(bytes) {
|
|
17
|
+
export function parseSliceYaml(bytes) {return validationMemo(parseCache,String(bytes),()=>parseSliceYamlFresh(bytes));}
|
|
18
|
+
function parseSliceYamlFresh(bytes) {
|
|
17
19
|
const doc = parseDocument(String(bytes), { uniqueKeys: true, intAsBigInt: true });
|
|
18
20
|
requireThat(!doc.errors.length, `YAML 无效: ${doc.errors[0]?.message}`);
|
|
19
21
|
const value = doc.toJS({ maxAliasCount: 0 });
|
|
@@ -99,10 +101,23 @@ export function sliceAcceptanceText(contract, sources) {
|
|
|
99
101
|
export function normalizeSliceContract(document, options = {}) {
|
|
100
102
|
const raw = sourceSliceContract(document);
|
|
101
103
|
requireThat(raw && [2,3].includes(raw.schema_version), 'Slice Implementation Contract schema v1 已停止支持;需要 schema v2 或 v3');
|
|
102
|
-
if (raw.schema_version === 2)
|
|
104
|
+
if (raw.schema_version === 2) {
|
|
105
|
+
const ref = raw.lifecycle_refs?.ticket;
|
|
106
|
+
if (ref) {
|
|
107
|
+
requireThat(!/\/work-items\//.test(ref), 'stage-work-item 不能作为实现 Ticket');
|
|
108
|
+
const file = safe(options.root || process.cwd(), ref, { missing: true });
|
|
109
|
+
if (fs.existsSync(file)) {
|
|
110
|
+
const header = fs.readFileSync(file, 'utf8').match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
111
|
+
requireThat(!header || parseSliceYaml(header[1])?.kind !== 'stage-work-item', 'stage-work-item 不能作为实现 Ticket');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return structuredClone(raw);
|
|
115
|
+
}
|
|
103
116
|
schema(raw,'docs/process/schemas/slice-implementation-contract-v3.schema.json');
|
|
104
117
|
const sources=readSliceSources(raw,options), refs=Object.fromEntries(Object.entries(sources).map(([key,item])=>[key,item.ref]));
|
|
105
118
|
requireKeys(refs,['spec','ticket','engineering_baseline','implementation_repository','build_architecture_checklist'],'依据');
|
|
119
|
+
const ticketHeader = sources.ticket.text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
120
|
+
requireThat(!/\/work-items\//.test(refs.ticket) && (!ticketHeader || parseSliceYaml(ticketHeader[1])?.kind !== 'stage-work-item'), 'stage-work-item 不能作为实现 Ticket');
|
|
106
121
|
if(!raw.applicability.checks)requireKeys(refs,['architecture_review'],'依据');
|
|
107
122
|
else {
|
|
108
123
|
requireKeys(refs,['lifecycle_registry','process_tailoring'],'适用性依据');
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import { withValidationPhase } from './validation-phase.mjs';
|
|
1
2
|
import { readSliceContract } from './slice-contract.mjs';
|
|
2
3
|
import { createApprovedExecutionContext } from './approved-execution-context.mjs';
|
|
3
4
|
import { evaluateContractFreshness, loadCompilerContract } from './implementation-contract-compiler.mjs';
|
|
4
5
|
import { loadSkillRegistry } from './skill-registry.mjs';
|
|
5
6
|
|
|
6
7
|
/** Readability, structure and execution approval are different levels of evidence. */
|
|
7
|
-
export function inspectSliceContract(ref,
|
|
8
|
+
export function inspectSliceContract(ref, options = {}) {
|
|
9
|
+
return withValidationPhase({root:options.root,purpose:'slice-inspect',slice_id:ref,work_unit_id:options.work_unit_id,readOnly:true,signal:options.signal},()=>inspect(ref,options));
|
|
10
|
+
}
|
|
11
|
+
function inspect(ref, { root = process.cwd(), approval_ref, work_unit_id } = {}) {
|
|
8
12
|
const loaded = readSliceContract(ref, { root });
|
|
9
13
|
const { contract, binding } = loaded;
|
|
10
14
|
const checks = { readable: 'passed', structure: 'passed', approval: 'not-checked', freshness: 'not-checked' };
|
|
@@ -28,9 +32,9 @@ export function inspectSliceContract(ref, { root = process.cwd(), approval_ref,
|
|
|
28
32
|
if (approval_ref && !blockers.length) {
|
|
29
33
|
try {
|
|
30
34
|
const approved_slice = { ...binding, approval_ref };
|
|
31
|
-
const context = createApprovedExecutionContext(approved_slice, { root, contract, work_unit_id });
|
|
35
|
+
const context = createApprovedExecutionContext(approved_slice, { root, contract, work_unit_id, readOnly: true });
|
|
32
36
|
checks.approval = 'passed';
|
|
33
|
-
const current = evaluateContractFreshness(contract, { root, registry: loadSkillRegistry(), compilerContract: loadCompilerContract(), approved_slice, work_unit_id }, context);
|
|
37
|
+
const current = evaluateContractFreshness(contract, { root, registry: loadSkillRegistry(), compilerContract: loadCompilerContract(), approved_slice, work_unit_id, readOnly: true }, context);
|
|
34
38
|
checks.freshness = current.freshness === 'current' ? 'passed' : 'failed';
|
|
35
39
|
blockers.push(...current.reasons);
|
|
36
40
|
} catch (error) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {enforceHarnessTaskScope} from './harness-execution-scope.mjs';
|
|
2
|
-
import { readFileSync } from '
|
|
2
|
+
import { readFileSync, withValidationPhase } from './validation-phase.mjs';
|
|
3
3
|
import { ROOT, hash, safe, digest } from './strategic-handoff-io.mjs';
|
|
4
4
|
import { normalizeSliceContract, withinSlicePath, parseSliceYaml } from './slice-contract.mjs';
|
|
5
5
|
import { createApprovedExecutionContext, assertApprovedExecutionContext } from './approved-execution-context.mjs';
|
|
@@ -14,7 +14,8 @@ function generateTaskPackageDefaults(roleId,overrides,{rolesDoc}={}) {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/** Compile dispatch from the approved YAML. Only runtime identity and task ID come from the caller. */
|
|
17
|
-
export function compileSliceTaskPackage(binding,{root
|
|
17
|
+
export function compileSliceTaskPackage(binding,options={}) {return withValidationPhase({root:options.root||ROOT,purpose:'slice-dispatch',slice_id:binding.ref,work_unit_id:options.work_unit_id,readOnly:false,signal:options.signal},()=>compile(binding,options));}
|
|
18
|
+
function compile(binding,{root=ROOT,work_unit_id,task_id,actor_id,runtime_id,execution_state='Worker',rolesDoc,user_decisions}={}) {
|
|
18
19
|
const actualRoles=parseSliceYaml(readFileSync(safe(root,'docs/agents/digital-human-roles.yaml')));
|
|
19
20
|
if(rolesDoc&&digest(rolesDoc)!==digest(actualRoles))fail('调用方不能替换接收端角色能力');
|
|
20
21
|
rolesDoc=actualRoles;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, unlinkSync, statSync, openSync, closeSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parseDocument, stringify } from '../vendor/yaml.mjs';
|
|
4
|
+
import { TRACKER_REF, STAGE_WORK_UNITS, sha256, safeTrackingPath, readTracking, parseYaml, trackerConfig, isDesign, binding, assertStageTracking, refreshTracking } from './stage-tracking.mjs';
|
|
5
|
+
|
|
6
|
+
const descriptor = (root, ref) => { const p = safeTrackingPath(root, ref); return existsSync(p) ? { digest: sha256(readFileSync(p)), mode: statSync(p).mode & 0o777 } : null; };
|
|
7
|
+
const json = x => `${JSON.stringify(x, null, 2)}\n`;
|
|
8
|
+
function featureFrom(ref) {
|
|
9
|
+
const match = /^docs\/\.scratch\/([a-z0-9][a-z0-9-]*)\/[^/]+\.(yaml|json)$/.exec(ref);
|
|
10
|
+
if (!match) throw new Error('tracking-checkpoint-path-invalid');
|
|
11
|
+
return match[1];
|
|
12
|
+
}
|
|
13
|
+
function identity(root) {
|
|
14
|
+
const value = parseYaml(readTracking(root, 'yss-project.yaml'));
|
|
15
|
+
if (value?.schema_version !== 1 || value.repository_mode !== 'project-instance') throw new Error('stage-tracking-project-instance-required');
|
|
16
|
+
}
|
|
17
|
+
function enabledTracker(bytes) {
|
|
18
|
+
const match = bytes.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
19
|
+
if (!match) throw new Error('tracking-tracker-frontmatter-required');
|
|
20
|
+
const doc = parseDocument(match[1], { uniqueKeys: true });
|
|
21
|
+
doc.setIn(['tracker', 'lifecycle_tracking_version'], 1);
|
|
22
|
+
return `---\n${doc.toString()}---${bytes.slice(match[0].length)}`;
|
|
23
|
+
}
|
|
24
|
+
function definition(item, checkpointRef) {
|
|
25
|
+
const fields = { kind: item.kind, id: item.id, checkpoint_ref: checkpointRef };
|
|
26
|
+
return `---\n${stringify(fields)}---\n# ${item.title}\n\n负责人:${item.owner}\n\n${item.scope}\n\n## 验收\n\n${item.acceptance.map(x => `- ${x}`).join('\n')}\n\n当前进度、依赖与完成证据以 ${checkpointRef} 中的 ${item.id} 为准。\n`;
|
|
27
|
+
}
|
|
28
|
+
export function checkTracking(root, checkpointRef) {
|
|
29
|
+
identity(root);
|
|
30
|
+
const feature = featureFrom(checkpointRef);
|
|
31
|
+
const config = trackerConfig(root);
|
|
32
|
+
if (!existsSync(safeTrackingPath(root, checkpointRef))) return { status: 'missing-checkpoint', feature_id: feature, enabled: config.lifecycle_tracking_version === 1 };
|
|
33
|
+
const checkpoint = parseYaml(readTracking(root, checkpointRef));
|
|
34
|
+
try {
|
|
35
|
+
const result = assertStageTracking(checkpoint, { root, checkpointRef });
|
|
36
|
+
return { ...result, feature_id: checkpoint.feature_id || feature, enabled: config.lifecycle_tracking_version === 1, migration_required: !checkpoint.stage_tracking };
|
|
37
|
+
} catch (error) { return { status: 'blocked', feature_id: feature, enabled: config.lifecycle_tracking_version === 1, error: error.message, migration_required: !checkpoint.stage_tracking }; }
|
|
38
|
+
}
|
|
39
|
+
export function planTracking(root, { checkpoint_ref, items = [], refresh = false } = {}) {
|
|
40
|
+
root = path.resolve(root); identity(root);
|
|
41
|
+
const feature = featureFrom(checkpoint_ref);
|
|
42
|
+
trackerConfig(root);
|
|
43
|
+
const changes = [], observed = {}, gaps = [];
|
|
44
|
+
function watch(ref) { observed[ref] = descriptor(root, ref); }
|
|
45
|
+
function change(ref, after) {
|
|
46
|
+
watch(ref);
|
|
47
|
+
if (!observed[ref] || sha256(after) !== observed[ref].digest) changes.push({ ref, before: observed[ref], after, after_digest: sha256(after) });
|
|
48
|
+
}
|
|
49
|
+
for (const ref of ['yss-project.yaml', 'CONTEXT.md', TRACKER_REF, 'docs/process/lifecycle-registry.yaml', 'docs/process/schemas/stage-tracking.schema.json', 'docs/process/harness-profile.yaml', checkpoint_ref]) watch(ref);
|
|
50
|
+
const exists = observed[checkpoint_ref] !== null;
|
|
51
|
+
let checkpoint;
|
|
52
|
+
if (exists) checkpoint = parseYaml(readTracking(root, checkpoint_ref));
|
|
53
|
+
else {
|
|
54
|
+
const template = 'docs/process/templates/lifecycle-checkpoint-template.yaml'; watch(template);
|
|
55
|
+
checkpoint = parseYaml(readTracking(root, template));
|
|
56
|
+
checkpoint.feature_id = feature;
|
|
57
|
+
checkpoint.mode = 'orchestrate'; checkpoint.status = 'running';
|
|
58
|
+
checkpoint.stage = items[0]?.stage || 'stage.plan';
|
|
59
|
+
checkpoint.next_work_unit = items[0]?.work_unit || 'work-unit.plan-requirements';
|
|
60
|
+
}
|
|
61
|
+
checkpoint.feature_id ??= feature;
|
|
62
|
+
const design = isDesign(root);
|
|
63
|
+
checkpoint.stage_tracking ??= { schema_version: 1, feature_id: checkpoint.feature_id, checkpoint_ref, entry_stage: checkpoint.stage,
|
|
64
|
+
entry: { kind: design ? 'checkpoint' : 'parent-ticket', ref: design ? checkpoint_ref : `docs/.scratch/${feature}/parent-ticket.md` }, items: [] };
|
|
65
|
+
if (refresh) checkpoint = refreshTracking(root, checkpoint).checkpoint;
|
|
66
|
+
const tracking = checkpoint.stage_tracking;
|
|
67
|
+
const ids = new Set(tracking.items.map(x => x.id));
|
|
68
|
+
for (const seed of items) {
|
|
69
|
+
if (ids.has(seed.id)) {
|
|
70
|
+
// Resume never overwrites a live item with an older task definition.
|
|
71
|
+
const existing = tracking.items.find(x => x.id === seed.id);
|
|
72
|
+
for (const key of ['title', 'owner', 'scope', 'stage', 'work_unit', 'acceptance', 'dependencies', 'split_reasons']) if (JSON.stringify(seed[key] ?? existing[key]) !== JSON.stringify(existing[key])) throw new Error(`tracking-existing-item-conflict: ${seed.id}.${key}`);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (seed.progress && seed.progress !== 'pending') throw new Error('tracking-migration-cannot-infer-completion');
|
|
76
|
+
const item = { id: seed.id, kind: 'stage-work-item', title: seed.title, stage: seed.stage, work_unit: seed.work_unit, owner: seed.owner, scope: seed.scope,
|
|
77
|
+
acceptance: seed.acceptance, dependencies: seed.dependencies || [], source_refs: (seed.source_refs || []).map(ref => binding(root, typeof ref === 'string' ? ref : ref.ref)),
|
|
78
|
+
progress: 'pending', split_reasons: seed.split_reasons || [], completion: [] };
|
|
79
|
+
if (seed.deferred) { item.deferred = seed.deferred; if (!item.split_reasons.includes('cross-stage-deferral')) item.split_reasons.push('cross-stage-deferral'); }
|
|
80
|
+
tracking.items.push(item); ids.add(item.id);
|
|
81
|
+
}
|
|
82
|
+
for (const item of tracking.items) {
|
|
83
|
+
if (tracking.items.some(other => other.dependencies.includes(item.id)) && !item.split_reasons.includes('blocks-other-work')) item.split_reasons.push('blocks-other-work');
|
|
84
|
+
if (tracking.items[0]?.owner && item.owner !== tracking.items[0].owner && !item.split_reasons.includes('different-owner')) item.split_reasons.push('different-owner');
|
|
85
|
+
}
|
|
86
|
+
if (!tracking.items.length) gaps.push('需要明确当前阶段工作项及负责人、范围和验收条件;不从历史文件推断完成');
|
|
87
|
+
if (!tracking.items.some(i => i.stage === checkpoint.stage) && ['stage.plan', 'stage.spec-architecture', 'stage.product-design'].includes(checkpoint.stage)) gaps.push('当前阶段缺少工作项');
|
|
88
|
+
for (const item of tracking.items) {
|
|
89
|
+
for (const key of ['id', 'title', 'owner', 'scope', 'stage', 'work_unit']) if (typeof item[key] !== 'string' || !item[key].trim()) gaps.push(`${item.id || '?'} 缺少 ${key}`);
|
|
90
|
+
if (!item.acceptance?.length) gaps.push(`${item.id || '?'} 缺少验收条件`);
|
|
91
|
+
for (const source of [...item.source_refs, ...item.completion.flatMap(x => x.evidence_refs)]) watch(source.ref);
|
|
92
|
+
if (item.deferred) watch(item.deferred.decision_ref);
|
|
93
|
+
if (item.split_reasons?.length && !gaps.length) {
|
|
94
|
+
const ref = `docs/.scratch/${feature}/work-items/${item.id}.md`;
|
|
95
|
+
if (!item.definition_ref) {
|
|
96
|
+
if (descriptor(root, ref)) throw new Error(`tracking-unowned-file-conflict: ${ref}`);
|
|
97
|
+
item.definition_ref = ref;
|
|
98
|
+
const content = definition(item, checkpoint_ref);
|
|
99
|
+
item.definition_digest = sha256(content); change(ref, content);
|
|
100
|
+
} else watch(item.definition_ref);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const entryRef = tracking.entry.ref;
|
|
104
|
+
if (!design) {
|
|
105
|
+
watch(entryRef);
|
|
106
|
+
const link = `\n阶段工作进度与证据:${checkpoint_ref}\n`;
|
|
107
|
+
if (!observed[entryRef]) change(entryRef, `# ${feature}\n\nStatus: needs-triage\n${link}${trackerConfig(root).platform !== 'local-markdown' ? `\npublication: pending\npending_publication_to: ${trackerConfig(root).platform}\n` : ''}`);
|
|
108
|
+
else if (!readTracking(root, entryRef).includes(checkpoint_ref)) change(entryRef, readTracking(root, entryRef) + link);
|
|
109
|
+
}
|
|
110
|
+
const mapRef = `docs/.scratch/${feature}/map.md`; watch(mapRef);
|
|
111
|
+
if (!observed[mapRef]) change(mapRef, `---\ncheckpoint_ref: ${checkpoint_ref}\n---\n# ${feature}\n\n阶段工作与证据见 ${checkpoint_ref}。\n`);
|
|
112
|
+
else if (!readTracking(root, mapRef).includes(checkpoint_ref)) change(mapRef, readTracking(root, mapRef) + `\n阶段工作与证据:${checkpoint_ref}\n`);
|
|
113
|
+
change(checkpoint_ref, checkpoint_ref.endsWith('.json') ? json(checkpoint) : stringify(checkpoint));
|
|
114
|
+
change(TRACKER_REF, enabledTracker(readTracking(root, TRACKER_REF)));
|
|
115
|
+
const input = { checkpoint_ref, items, refresh };
|
|
116
|
+
const payload = { schema_version: 1, root, input, gaps, observed, changes };
|
|
117
|
+
return { ...payload, plan_id: sha256(JSON.stringify(payload)).slice(7) };
|
|
118
|
+
}
|
|
119
|
+
export function applyTracking(root, plan, { afterWrite } = {}) {
|
|
120
|
+
root = path.resolve(root); identity(root);
|
|
121
|
+
if (plan.root !== root || plan.schema_version !== 1 || !/^[a-f0-9]{64}$/.test(plan.plan_id)) throw new Error('tracking-plan-identity-invalid');
|
|
122
|
+
const { plan_id, ...payload } = plan;
|
|
123
|
+
if (sha256(JSON.stringify(payload)).slice(7) !== plan_id) throw new Error('tracking-plan-digest-mismatch');
|
|
124
|
+
const feature = featureFrom(plan.input.checkpoint_ref);
|
|
125
|
+
const transactionRef = `docs/.scratch/${feature}/verification/stage-tracking-migrations/${plan_id}`;
|
|
126
|
+
const receiptRef = `${transactionRef}/receipt.json`;
|
|
127
|
+
if (existsSync(safeTrackingPath(root, receiptRef))) {
|
|
128
|
+
const receipt = JSON.parse(readTracking(root, receiptRef));
|
|
129
|
+
if (receipt.plan_id !== plan_id) throw new Error('tracking-receipt-conflict');
|
|
130
|
+
return { status: 'unchanged', plan_id, receipt_ref: receiptRef };
|
|
131
|
+
}
|
|
132
|
+
if (plan.gaps.length) throw new Error(`tracking-migration-needs-info: ${plan.gaps.join('; ')}`);
|
|
133
|
+
const fresh = planTracking(root, plan.input);
|
|
134
|
+
if (JSON.stringify(fresh) !== JSON.stringify(plan)) throw new Error('tracking-plan-stale');
|
|
135
|
+
if (!plan.changes.length) return { status: 'unchanged', plan_id };
|
|
136
|
+
const transactionPath = safeTrackingPath(root, transactionRef);
|
|
137
|
+
if (existsSync(transactionPath)) throw new Error(`tracking-transaction-needs-recovery: ${transactionRef}`);
|
|
138
|
+
mkdirSync(transactionPath, { recursive: true });
|
|
139
|
+
const backups = plan.changes.map(c => ({ ref: c.ref, before: c.before, bytes: c.before ? readFileSync(safeTrackingPath(root, c.ref)).toString('base64') : null }));
|
|
140
|
+
writeFileSync(path.join(transactionPath, 'backup.json'), json(backups), { flag: 'wx' });
|
|
141
|
+
writeFileSync(path.join(transactionPath, 'plan.json'), json(plan), { flag: 'wx' });
|
|
142
|
+
const written = [], temporary = new Set();
|
|
143
|
+
try {
|
|
144
|
+
for (const [ref, before] of Object.entries(plan.observed)) if (JSON.stringify(descriptor(root, ref)) !== JSON.stringify(before)) throw new Error('tracking-concurrent-change');
|
|
145
|
+
for (const change of plan.changes) {
|
|
146
|
+
const file = safeTrackingPath(root, change.ref);
|
|
147
|
+
if (JSON.stringify(descriptor(root, change.ref)) !== JSON.stringify(change.before)) throw new Error('tracking-concurrent-change');
|
|
148
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
149
|
+
const temp = `${file}.stage-tracking-${plan_id}`;
|
|
150
|
+
const fd = openSync(temp, 'wx', change.before?.mode || 0o644);
|
|
151
|
+
temporary.add(temp);
|
|
152
|
+
try { writeFileSync(fd, change.after); } finally { closeSync(fd); }
|
|
153
|
+
renameSync(temp, file); temporary.delete(temp); written.push(change);
|
|
154
|
+
afterWrite?.(change, written.length);
|
|
155
|
+
}
|
|
156
|
+
for (const [ref, before] of Object.entries(plan.observed)) {
|
|
157
|
+
const changed = written.find(x => x.ref === ref);
|
|
158
|
+
const actual = descriptor(root, ref);
|
|
159
|
+
if (changed ? actual?.digest !== changed.after_digest : JSON.stringify(actual) !== JSON.stringify(before)) throw new Error('tracking-concurrent-change');
|
|
160
|
+
}
|
|
161
|
+
const cp = parseYaml(readTracking(root, plan.input.checkpoint_ref));
|
|
162
|
+
assertStageTracking(cp, { root, checkpointRef: plan.input.checkpoint_ref });
|
|
163
|
+
writeFileSync(safeTrackingPath(root, receiptRef), json({ plan_id, status: 'applied', changed_refs: written.map(x => x.ref) }), { flag: 'wx' });
|
|
164
|
+
return { status: 'applied', plan_id, receipt_ref: receiptRef, changed_refs: written.map(x => x.ref) };
|
|
165
|
+
} catch (error) {
|
|
166
|
+
const conflicts = [];
|
|
167
|
+
for (const temp of temporary) { try { unlinkSync(temp); } catch (cleanupError) { if (cleanupError.code !== 'ENOENT') conflicts.push(temp); } }
|
|
168
|
+
for (const change of written.reverse()) {
|
|
169
|
+
if (descriptor(root, change.ref)?.digest !== change.after_digest) { conflicts.push(change.ref); continue; }
|
|
170
|
+
const backup = backups.find(x => x.ref === change.ref), file = safeTrackingPath(root, change.ref);
|
|
171
|
+
if (backup.bytes === null) unlinkSync(file); else writeFileSync(file, Buffer.from(backup.bytes, 'base64'), { mode: backup.before.mode });
|
|
172
|
+
}
|
|
173
|
+
writeFileSync(path.join(transactionPath, 'failure.json'), json({ error: error.message, recovery_conflicts: conflicts }));
|
|
174
|
+
throw new Error(`${error.message}; tracking-rollback-${conflicts.length ? `conflict:${conflicts.join(',')}` : 'complete'}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { readFileSync, existsSync, lstatSync, realpathSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { parseDocument } from '../vendor/yaml.mjs';
|
|
5
|
+
import { validateJsonSchema } from './json-schema.mjs';
|
|
6
|
+
|
|
7
|
+
export const TRACKER_REF = 'docs/agents/issue-tracker.md';
|
|
8
|
+
export const STAGE_WORK_UNITS = Object.freeze({
|
|
9
|
+
'work-unit.plan-opportunity': 'stage.plan',
|
|
10
|
+
'work-unit.plan-requirements': 'stage.plan',
|
|
11
|
+
'work-unit.domain-strategy-design': 'stage.plan',
|
|
12
|
+
'work-unit.stage-decision': 'stage.plan',
|
|
13
|
+
'work-unit.spec-synthesis': 'stage.spec-architecture',
|
|
14
|
+
'work-unit.prototype-design': 'stage.product-design',
|
|
15
|
+
});
|
|
16
|
+
export const sha256 = bytes => `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
17
|
+
export function parseYaml(bytes) {
|
|
18
|
+
const doc = parseDocument(String(bytes), { uniqueKeys: true });
|
|
19
|
+
if (doc.errors.length) throw new Error(doc.errors[0].message);
|
|
20
|
+
return doc.toJS({ maxAliasCount: 0 });
|
|
21
|
+
}
|
|
22
|
+
export function safeTrackingPath(root, ref) {
|
|
23
|
+
if (typeof ref !== 'string' || !ref || path.isAbsolute(ref) || ref.includes('\\') || ref.split('/').some(p => !p || p === '.' || p === '..')) throw new Error(`tracking-path-invalid: ${ref}`);
|
|
24
|
+
const base = realpathSync(root);
|
|
25
|
+
let file = base;
|
|
26
|
+
for (const part of ref.split('/')) {
|
|
27
|
+
file = path.join(file, part);
|
|
28
|
+
if (existsSync(file) || (() => { try { lstatSync(file); return true; } catch { return false; } })()) {
|
|
29
|
+
if (lstatSync(file).isSymbolicLink()) throw new Error(`tracking-symlink-forbidden: ${ref}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return file;
|
|
33
|
+
}
|
|
34
|
+
export const readTracking = (root, ref) => readFileSync(safeTrackingPath(root, ref), 'utf8');
|
|
35
|
+
export const binding = (root, ref) => ({ ref, digest: sha256(readTracking(root, ref)) });
|
|
36
|
+
export function trackerConfig(root) {
|
|
37
|
+
const bytes = readTracking(root, TRACKER_REF);
|
|
38
|
+
const match = bytes.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
39
|
+
if (!match) throw new Error('tracking-tracker-frontmatter-required');
|
|
40
|
+
const config = parseYaml(match[1])?.tracker;
|
|
41
|
+
if (!config || !['local-markdown', 'github', 'gitlab'].includes(config.platform)) throw new Error('tracking-tracker-invalid');
|
|
42
|
+
if (config.lifecycle_tracking_version !== undefined && config.lifecycle_tracking_version !== 1) throw new Error('tracking-version-unsupported');
|
|
43
|
+
return config;
|
|
44
|
+
}
|
|
45
|
+
export function trackingProfile(root) {
|
|
46
|
+
const ref = 'docs/process/harness-profile.yaml';
|
|
47
|
+
return existsSync(safeTrackingPath(root, ref)) ? parseYaml(readTracking(root, ref)) : null;
|
|
48
|
+
}
|
|
49
|
+
export const isDesign = root => trackingProfile(root)?.profile_id === 'harness.business-ddd-strategy-handoff';
|
|
50
|
+
function current(root, value) {
|
|
51
|
+
try { return binding(root, value.ref).digest === value.digest; } catch { return false; }
|
|
52
|
+
}
|
|
53
|
+
export function trackingDrift(root, tracking) {
|
|
54
|
+
const changed = new Set(tracking.items.filter(item => [...item.source_refs, ...item.completion.flatMap(c => c.evidence_refs)].some(b => !current(root, b))).map(item => item.id));
|
|
55
|
+
let size;
|
|
56
|
+
do {
|
|
57
|
+
size = changed.size;
|
|
58
|
+
for (const item of tracking.items) if (item.dependencies.some(id => changed.has(id))) changed.add(item.id);
|
|
59
|
+
} while (size !== changed.size);
|
|
60
|
+
return [...changed];
|
|
61
|
+
}
|
|
62
|
+
export function refreshTracking(root, checkpoint) {
|
|
63
|
+
const next = structuredClone(checkpoint);
|
|
64
|
+
const changed = trackingDrift(root, next.stage_tracking);
|
|
65
|
+
for (const item of next.stage_tracking.items) {
|
|
66
|
+
if (changed.includes(item.id) && item.progress !== 'cancelled') {
|
|
67
|
+
item.progress = 'pending';
|
|
68
|
+
item.recheck_required = true;
|
|
69
|
+
// Old bindings and evidence remain historical; only explicit re-verification replaces them.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { checkpoint: next, stale_item_ids: changed };
|
|
73
|
+
}
|
|
74
|
+
export function assertStageTracking(checkpoint, { root, checkpointRef, currentWorkUnit, nextWorkUnit, transition = false, entering = false, now = new Date() } = {}) {
|
|
75
|
+
const identity = parseYaml(readTracking(root, 'yss-project.yaml'));
|
|
76
|
+
if (identity?.schema_version !== 1 || !['template-source', 'project-instance'].includes(identity.repository_mode)) throw new Error('tracking-repository-identity-invalid');
|
|
77
|
+
const config = trackerConfig(root);
|
|
78
|
+
const tracking = checkpoint?.stage_tracking;
|
|
79
|
+
const relevant = STAGE_WORK_UNITS[currentWorkUnit] || STAGE_WORK_UNITS[nextWorkUnit] || ['stage.plan', 'stage.spec-architecture', 'stage.product-design'].includes(checkpoint?.stage);
|
|
80
|
+
if (!tracking) {
|
|
81
|
+
if (identity.repository_mode === 'project-instance' && config.lifecycle_tracking_version === 1 && relevant) throw new Error('stage-tracking-required');
|
|
82
|
+
return { status: 'not-applicable', stale_item_ids: [] };
|
|
83
|
+
}
|
|
84
|
+
validateJsonSchema(tracking, path.join(root, 'docs/process/schemas/stage-tracking.schema.json'));
|
|
85
|
+
const feature = checkpoint.feature_id;
|
|
86
|
+
if (feature !== tracking.feature_id || typeof feature !== 'string' || !feature.trim()) throw new Error('tracking-feature-mismatch');
|
|
87
|
+
const location = /^(docs\/\.scratch\/[a-z0-9][a-z0-9-]*\/)[^/]+\.(yaml|json)$/.exec(tracking.checkpoint_ref);
|
|
88
|
+
if (!location) throw new Error('tracking-checkpoint-path-invalid');
|
|
89
|
+
const base = location[1];
|
|
90
|
+
if (!tracking.checkpoint_ref.startsWith(base) || (checkpointRef && tracking.checkpoint_ref !== checkpointRef)) throw new Error('tracking-checkpoint-mismatch');
|
|
91
|
+
safeTrackingPath(root, tracking.checkpoint_ref);
|
|
92
|
+
if (isDesign(root)) {
|
|
93
|
+
if (tracking.entry.kind !== 'checkpoint' || tracking.entry.ref !== tracking.checkpoint_ref) throw new Error('tracking-design-parent-forbidden');
|
|
94
|
+
} else if (tracking.entry.kind !== 'parent-ticket' || tracking.entry.ref !== `${base}parent-ticket.md`) throw new Error('tracking-parent-required');
|
|
95
|
+
const entry = readTracking(root, tracking.entry.ref);
|
|
96
|
+
if (tracking.entry.kind === 'parent-ticket' && !entry.includes(tracking.checkpoint_ref)) throw new Error('tracking-parent-checkpoint-link-required');
|
|
97
|
+
const registry = parseYaml(readTracking(root, 'docs/process/lifecycle-registry.yaml'));
|
|
98
|
+
const stages = new Set(registry.stages.map(x => x.id));
|
|
99
|
+
const units = new Set(registry.work_units.map(x => x.id));
|
|
100
|
+
if (!stages.has(tracking.entry_stage)) throw new Error('tracking-entry-stage-invalid');
|
|
101
|
+
const profile = trackingProfile(root);
|
|
102
|
+
const allowed = profile?.lifecycle?.allowed_work_units ?? profile?.allowed_work_units;
|
|
103
|
+
const index = new Map();
|
|
104
|
+
for (const item of tracking.items) {
|
|
105
|
+
if (index.has(item.id)) throw new Error(`tracking-duplicate-id: ${item.id}`);
|
|
106
|
+
index.set(item.id, item);
|
|
107
|
+
if (!stages.has(item.stage) || !units.has(item.work_unit) || STAGE_WORK_UNITS[item.work_unit] !== item.stage) throw new Error(`tracking-stage-work-unit-mismatch: ${item.id}`);
|
|
108
|
+
if (allowed && !allowed.includes(item.work_unit)) throw new Error(`tracking-profile-work-unit-forbidden: ${item.id}`);
|
|
109
|
+
if (item.split_reasons.length && !item.definition_ref) throw new Error(`tracking-independent-item-required: ${item.id}`);
|
|
110
|
+
if (item.definition_ref) {
|
|
111
|
+
if (item.definition_ref !== `${base}work-items/${item.id}.md`) throw new Error('tracking-definition-path-invalid');
|
|
112
|
+
const definition = readTracking(root, item.definition_ref);
|
|
113
|
+
if (!definition.includes(`kind: stage-work-item`) || !definition.includes(`id: ${item.id}\n`) || !definition.includes(tracking.checkpoint_ref) || /ready-for-agent|^Status:|^progress:/m.test(definition)) throw new Error('tracking-definition-invalid');
|
|
114
|
+
if (item.definition_digest !== sha256(definition)) throw new Error('tracking-definition-drift');
|
|
115
|
+
}
|
|
116
|
+
if (item.deferred) {
|
|
117
|
+
if (!item.split_reasons.includes('cross-stage-deferral') || !item.definition_ref) throw new Error('tracking-deferral-independent-item-required');
|
|
118
|
+
if (item.progress === 'completed' || item.progress === 'cancelled') throw new Error('tracking-deferred-not-completed');
|
|
119
|
+
if (!Number.isFinite(Date.parse(item.deferred.resolve_by)) || Date.parse(item.deferred.resolve_by) <= now.getTime()) throw new Error('tracking-deferral-expired');
|
|
120
|
+
readTracking(root, item.deferred.decision_ref);
|
|
121
|
+
}
|
|
122
|
+
if (item.progress === 'cancelled' && !item.cancellation_reason) throw new Error('tracking-cancellation-reason-required');
|
|
123
|
+
if (item.progress === 'completed') {
|
|
124
|
+
if (!item.source_refs.length) throw new Error('tracking-completion-source-required');
|
|
125
|
+
if (item.recheck_required) throw new Error('tracking-recheck-required');
|
|
126
|
+
for (const criterion of item.acceptance) if (!item.completion.some(c => c.criterion === criterion && c.evidence_refs.length)) throw new Error(`tracking-acceptance-evidence-required: ${item.id}`);
|
|
127
|
+
for (const c of item.completion) if (!item.acceptance.includes(c.criterion)) throw new Error('tracking-unknown-acceptance-criterion');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const visited = new Set(), visiting = new Set();
|
|
131
|
+
function visit(id) {
|
|
132
|
+
if (visiting.has(id)) throw new Error('tracking-dependency-cycle');
|
|
133
|
+
if (visited.has(id)) return;
|
|
134
|
+
const item = index.get(id);
|
|
135
|
+
if (!item) throw new Error(`tracking-dependency-missing: ${id}`);
|
|
136
|
+
visiting.add(id); item.dependencies.forEach(visit); visiting.delete(id); visited.add(id);
|
|
137
|
+
}
|
|
138
|
+
index.forEach((_, id) => visit(id));
|
|
139
|
+
for (const item of tracking.items) {
|
|
140
|
+
if ((item.owner !== tracking.items[0].owner || tracking.items.some(other => other.dependencies.includes(item.id))) && !item.definition_ref) throw new Error(`tracking-independent-item-required: ${item.id}`);
|
|
141
|
+
|
|
142
|
+
if (['running', 'completed'].includes(item.progress) && item.dependencies.some(id => index.get(id).progress !== 'completed')) throw new Error(`tracking-dependency-blocked: ${item.id}`);
|
|
143
|
+
}
|
|
144
|
+
const stale = trackingDrift(root, tracking);
|
|
145
|
+
if (stale.some(id => ['running', 'completed'].includes(index.get(id).progress))) throw new Error(`tracking-stale-completion: ${stale.join(',')}`);
|
|
146
|
+
if (entering && STAGE_WORK_UNITS[nextWorkUnit] && !tracking.items.some(i => i.work_unit === nextWorkUnit)) throw new Error('tracking-entry-work-item-required');
|
|
147
|
+
if (transition && STAGE_WORK_UNITS[currentWorkUnit]) {
|
|
148
|
+
const items = tracking.items.filter(i => i.work_unit === currentWorkUnit);
|
|
149
|
+
if (!items.length) throw new Error('tracking-current-work-item-required');
|
|
150
|
+
if (items.some(i => !['completed', 'cancelled'].includes(i.progress) && !i.deferred)) throw new Error('tracking-current-work-incomplete');
|
|
151
|
+
if (STAGE_WORK_UNITS[currentWorkUnit] !== STAGE_WORK_UNITS[nextWorkUnit] && tracking.items.some(i => i.stage === STAGE_WORK_UNITS[currentWorkUnit] && !['completed', 'cancelled'].includes(i.progress) && !i.deferred)) throw new Error('tracking-stage-incomplete');
|
|
152
|
+
}
|
|
153
|
+
return { status: 'valid', stale_item_ids: stale };
|
|
154
|
+
}
|
|
155
|
+
export function assertTrackingTransition(currentWorkUnit, nextWorkUnit, state, { root } = {}) {
|
|
156
|
+
// Persisted checkpoint is authoritative. Inline WER status cannot replace it.
|
|
157
|
+
if (!STAGE_WORK_UNITS[currentWorkUnit] && !STAGE_WORK_UNITS[nextWorkUnit] && !state?.stage_tracking && !state?.checkpoint_ref) return { status: 'not-applicable', stale_item_ids: [] };
|
|
158
|
+
const repository = parseYaml(readTracking(root, 'yss-project.yaml'));
|
|
159
|
+
if (repository.repository_mode === 'template-source' && !state?.stage_tracking) return { status: 'not-applicable', stale_item_ids: [] };
|
|
160
|
+
if (!state?.checkpoint_ref) {
|
|
161
|
+
const identity = parseYaml(readTracking(root, 'yss-project.yaml'));
|
|
162
|
+
if (state?.stage_tracking || (identity.repository_mode === 'project-instance' && trackerConfig(root).lifecycle_tracking_version === 1 && (STAGE_WORK_UNITS[currentWorkUnit] || STAGE_WORK_UNITS[nextWorkUnit]))) throw new Error('tracking-persisted-checkpoint-ref-required');
|
|
163
|
+
return assertStageTracking(state, { root, currentWorkUnit, nextWorkUnit, transition: true });
|
|
164
|
+
}
|
|
165
|
+
const checkpoint = parseYaml(readTracking(root, state.checkpoint_ref));
|
|
166
|
+
if (state.feature_id && checkpoint.feature_id !== state.feature_id) throw new Error('tracking-feature-mismatch');
|
|
167
|
+
return assertStageTracking(checkpoint, { root, checkpointRef: state.checkpoint_ref, currentWorkUnit, nextWorkUnit, transition: true });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function assertTrackingEntry(workUnit, state, { root } = {}) {
|
|
171
|
+
if (!STAGE_WORK_UNITS[workUnit]) return;
|
|
172
|
+
const repository = parseYaml(readTracking(root, 'yss-project.yaml'));
|
|
173
|
+
if (repository.repository_mode === 'template-source') return;
|
|
174
|
+
const enabled = trackerConfig(root).lifecycle_tracking_version === 1;
|
|
175
|
+
if (!state?.checkpoint_ref) {
|
|
176
|
+
if (enabled) throw new Error('tracking-entry-checkpoint-required');
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const checkpoint = parseYaml(readTracking(root, state.checkpoint_ref));
|
|
180
|
+
assertStageTracking(checkpoint, { root, checkpointRef: state.checkpoint_ref, nextWorkUnit: workUnit, entering: true });
|
|
181
|
+
const items = checkpoint.stage_tracking?.items.filter(i => i.work_unit === workUnit) || [];
|
|
182
|
+
const stale = new Set(checkpoint.stage_tracking ? trackingDrift(root, checkpoint.stage_tracking) : []);
|
|
183
|
+
if (items.length && !items.some(i => !['blocked', 'cancelled'].includes(i.progress) && !i.deferred && !i.recheck_required && !stale.has(i.id) && i.dependencies.every(id => checkpoint.stage_tracking.items.find(d => d.id === id)?.progress === 'completed'))) throw new Error('tracking-entry-no-runnable-work');
|
|
184
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { uiBaselineSourceKeys, uiBaselineKind } from './ui-baseline.mjs';
|
|
2
|
-
import { existsSync, readdirSync } from '
|
|
2
|
+
import { existsSync, readdirSync, inValidationPhase, acceptValidationDependencies } from './validation-phase.mjs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
5
|
import { openBundle } from './strategic-handoff.mjs';
|
|
@@ -29,8 +29,9 @@ export async function verifyConsumption(data,{root=process.cwd(),sliceRef,consum
|
|
|
29
29
|
const route=receipt.routes.find(item=>item.capability===capability);
|
|
30
30
|
ensure(route&&route.activation!=='not-applicable'&&binding.route_id===route.route_id,`${capability} 未绑定当前 Handoff v4/v5 route_id`);
|
|
31
31
|
}
|
|
32
|
-
const result=spawnSync(process.execPath,[path.join(ROOT,'scripts/verify-context-reconciliation'),'--root',root,safe(root,binding.context_reconciliation_ref)],{encoding:'utf8'});
|
|
32
|
+
const result=spawnSync(process.execPath,[path.join(ROOT,'scripts/verify-context-reconciliation'),'--root',root,safe(root,binding.context_reconciliation_ref),...(inValidationPhase()?['--validation-dependencies']:[])],{encoding:'utf8',timeout:60000,maxBuffer:16*1024*1024});
|
|
33
33
|
ensure(result.status===0,`目标术语对账未通过: ${result.error?.message || result.stderr}`);
|
|
34
|
+
if(inValidationPhase()){let value;try{value=JSON.parse(result.stdout);}catch{throw new TypeError('VALIDATION_RECEIPT_INVALID: Context 子进程未返回依赖');}acceptValidationDependencies(value.validation_dependencies);}
|
|
34
35
|
const context=parseContextContract({root});
|
|
35
36
|
const parent=safe(root,`docs/handoffs/${receipt.bundle_id}`);
|
|
36
37
|
const versions=readdirSync(parent).filter(x=>/^v[1-9][0-9]*$/.test(x)&&existsSync(path.join(parent,x,'import-receipt.json'))).sort((a,b)=>Number(b.slice(1))-Number(a.slice(1)));
|
|
@@ -84,6 +85,6 @@ export async function verifyConsumption(data,{root=process.cwd(),sliceRef,consum
|
|
|
84
85
|
}
|
|
85
86
|
const blockedHere=all||(sliceRef?blocked.has(sliceRef):issues.length>0);
|
|
86
87
|
return {result:blockedHere?'blocked':'verified',bundle_digest:current.manifest.bundle_digest,consumed_bundle_digest:receipt.bundle_digest,...(consumer==='frontend'?{frontend_acceptance_digest:digest(data)}:{tactical_digest:digest(data), ...(data.schema_version === 2 ? {technical_design_digest:digest(data)} : {})}),scope:sliceRef||`whole-${consumer}-design`,coverage_complete:[...known.keys()].every(id=>rows.has(id)),block_all:all,blocked_slice_refs:[...blocked].sort(),issues};
|
|
87
|
-
},{readOnly});
|
|
88
|
-
},{readOnly});
|
|
88
|
+
},{readOnly:readOnly||inValidationPhase()});
|
|
89
|
+
},{readOnly:readOnly||inValidationPhase()});
|
|
89
90
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { mkdirSync, writeFileSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, validationMemo } from './validation-phase.mjs';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
5
6
|
import { spawnSync } from 'node:child_process';
|
|
@@ -9,6 +10,7 @@ import { parseDocument } from '../vendor/yaml.mjs';
|
|
|
9
10
|
export const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
|
10
11
|
// A v5 package keeps the source glossary as a snapshot, never a nested CONTEXT.md.
|
|
11
12
|
// Only this fixed alias is supported, isolated to the current asynchronous reader.
|
|
13
|
+
const parseCache=Symbol('handoff-yaml');
|
|
12
14
|
const sourceContextSnapshots = new AsyncLocalStorage();
|
|
13
15
|
export function withSourceContextSnapshot(root, action) {
|
|
14
16
|
const roots = new Set(sourceContextSnapshots.getStore() || []);
|
|
@@ -20,7 +22,8 @@ export const canonical = value => Array.isArray(value) ? value.map(canonical) :
|
|
|
20
22
|
export const hash = bytes => `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
21
23
|
export const digest = value => hash(JSON.stringify(canonical(value)));
|
|
22
24
|
export const json = value => `${JSON.stringify(canonical(value), null, 2)}\n`;
|
|
23
|
-
export function parse(bytes) {
|
|
25
|
+
export function parse(bytes) {return validationMemo(parseCache,String(bytes),()=>parseFresh(bytes));}
|
|
26
|
+
function parseFresh(bytes) {
|
|
24
27
|
const doc = parseDocument(String(bytes), { uniqueKeys: true, maxAliasCount: 0 });
|
|
25
28
|
ensure(!doc.errors.length, `YAML/JSON 无效: ${doc.errors[0]?.message}`);
|
|
26
29
|
return doc.toJS({ maxAliasCount: 0 });
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { existsSync } from './validation-phase.mjs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { read, safe, ensure } from './strategic-handoff-io.mjs';
|
|
4
|
+
|
|
5
|
+
// Wire work-unit IDs belong to the immutable source package. Only target routing is adapted.
|
|
6
|
+
export function consumerEntry(root, capability, sourceWorkUnit) {
|
|
7
|
+
const scope = existsSync(path.join(root, '.yss-execution-scope.yaml')) ? read(safe(root, '.yss-execution-scope.yaml')) : null;
|
|
8
|
+
const profile = existsSync(path.join(root, 'docs/process/harness-profile.yaml')) ? read(safe(root, 'docs/process/harness-profile.yaml')) : null;
|
|
9
|
+
const contractRef = scope || !profile ? '.agents/skills/yss-product-lifecycle/references/orchestration-contract.yaml' : '.agents/skills/harness-orchestrator/references/orchestration-contract.yaml';
|
|
10
|
+
const contract = read(safe(root, contractRef));
|
|
11
|
+
const target = scope?.scope_id || profile?.profile_id || 'yss-full-lifecycle';
|
|
12
|
+
const policy = contract.consumer_entry_routes?.[target]?.[capability];
|
|
13
|
+
ensure(policy && policy.source_work_unit === sourceWorkUnit, `consumer-entry-unmapped: ${target}/${capability}/${sourceWorkUnit}`);
|
|
14
|
+
const registry = read(safe(root, 'docs/process/lifecycle-registry.yaml'));
|
|
15
|
+
ensure(registry.work_units.some(unit => unit.id === policy.target_work_unit), 'consumer-entry-unknown-target');
|
|
16
|
+
if (scope) {
|
|
17
|
+
ensure(scope.schema_version === 1, 'consumer-entry-invalid-scope');
|
|
18
|
+
ensure(contract.execution_scopes?.[scope.scope_id]?.allowed_work_units?.includes(policy.target_work_unit), 'consumer-entry-outside-scope');
|
|
19
|
+
}
|
|
20
|
+
if (profile) ensure(profile.lifecycle.allowed_work_units.includes(policy.target_work_unit), 'consumer-entry-outside-profile');
|
|
21
|
+
return { capability, source_work_unit: sourceWorkUnit, target_work_unit: policy.target_work_unit, target_profile: target };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function scopedConsumerCapabilities(root, fallback) {
|
|
25
|
+
if (!existsSync(path.join(root, '.yss-execution-scope.yaml'))) return fallback;
|
|
26
|
+
const scope = read(safe(root, '.yss-execution-scope.yaml'));
|
|
27
|
+
const contract = read(safe(root, '.agents/skills/yss-product-lifecycle/references/orchestration-contract.yaml'));
|
|
28
|
+
const capabilities = contract.execution_scopes?.[scope.scope_id]?.consumer_capabilities;
|
|
29
|
+
ensure(scope.schema_version === 1 && Array.isArray(capabilities) && capabilities.length, 'consumer-scope-capabilities-missing');
|
|
30
|
+
return capabilities;
|
|
31
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { scopedConsumerCapabilities } from './strategic-handoff-routing.mjs';
|
|
2
|
+
import { existsSync, lstatSync, readFileSync } from './validation-phase.mjs';
|
|
3
|
+
import { mkdirSync, mkdtempSync, renameSync, rmSync, cpSync } from 'node:fs';
|
|
2
4
|
import path from 'node:path';
|
|
3
5
|
import { tmpdir } from 'node:os';
|
|
4
6
|
import { ensure, canonical, digest, hash, json, parse, read, safe, relative, files, write, project, schema, schemaBatch, archive, ROOT, sourceApprovalPolicy, withSourceContextSnapshot } from './strategic-handoff-io.mjs';
|
|
@@ -432,7 +434,7 @@ export async function importBundle({bundle,targetRoot}) {
|
|
|
432
434
|
}else{
|
|
433
435
|
const profileRef=path.join(target,'docs/process/harness-profile.yaml');
|
|
434
436
|
const profile=existsSync(profileRef)?read(profileRef):null;
|
|
435
|
-
const capabilities=profile?.handoff?.consumer_capabilities||CAPABILITIES;
|
|
437
|
+
const capabilities=scopedConsumerCapabilities(target,profile?.handoff?.consumer_capabilities||CAPABILITIES);
|
|
436
438
|
ensure(Array.isArray(capabilities)&&capabilities.length&&capabilities.every(capability=>CAPABILITIES.includes(capability)),'目标 profile 未声明受支持的消费者能力');
|
|
437
439
|
const selected=b.handoff.consumer_routes.filter(route=>capabilities.includes(route.capability));
|
|
438
440
|
ensure(selected.length===capabilities.length,'目标 profile 的消费者能力在 Handoff v4 中缺失');
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertTrackingEntry } from './stage-tracking.mjs';
|
|
1
2
|
import { normalizeSliceContract } from './slice-contract.mjs';
|
|
2
3
|
import { assertSliceV3TaskPackage } from './slice-task-package.mjs';
|
|
3
4
|
export { compileSliceTaskPackage, assertSliceV3TaskPackage } from './slice-task-package.mjs';
|
|
@@ -68,6 +69,7 @@ function validateSkillSource(value, registry) {
|
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
function validateCommon(value, registry, lifecycle) {
|
|
72
|
+
if (value.allowed_write_paths?.length && value.contract.kind === "lifecycle-work-unit") assertTrackingEntry(value.work_unit_id, value, { root: ROOT });
|
|
71
73
|
if (value.work_unit_id === 'work-unit.spec-synthesis') assertPlanSpecEntry(value);
|
|
72
74
|
const workUnit = lifecycle.work_units.find((item) => item.id === value.work_unit_id);
|
|
73
75
|
if (!workUnit && value.contract.kind !== "slice-implementation") fail(`未知 work_unit_id: ${value.work_unit_id}`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { assertApprovedExecutionContext, approvedExecutionBinding } from './approved-execution-context.mjs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { readFileSync } from '
|
|
3
|
+
import { readFileSync, inValidationPhase, acceptValidationDependencies } from './validation-phase.mjs';
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
5
|
import { ROOT, read, safe, ensure, hash } from './strategic-handoff-io.mjs';
|
|
6
6
|
|
|
@@ -22,11 +22,13 @@ export function enforceTechnicalDesign(state = {}, { root = ROOT, execution, rea
|
|
|
22
22
|
if (binding.legacy_ddd === true) args.push('--legacy-ddd');
|
|
23
23
|
if(readOnly)args.push('--read-only');
|
|
24
24
|
if(execution){
|
|
25
|
-
assertApprovedExecutionContext(execution,{root,technicalDesign:data,architectureIdentity:state.architecture_identity,sliceId:state.slice_id});
|
|
25
|
+
assertApprovedExecutionContext(execution,{root,technicalDesign:data,architectureIdentity:state.architecture_identity,sliceId:state.slice_id,readOnly});
|
|
26
26
|
const approved=approvedExecutionBinding(execution);
|
|
27
27
|
if(approved.work_unit_id)args.push('--work-unit',approved.work_unit_id);
|
|
28
28
|
args.push('--approved-slice',approved.ref,'--approved-slice-digest',approved.digest,'--approved-slice-approval',approved.approval_ref);
|
|
29
29
|
}
|
|
30
|
+
if(inValidationPhase())args.push('--validation-dependencies');
|
|
30
31
|
const verified = spawnSync(process.execPath, args, { cwd: root, encoding: 'utf8', timeout: 60000, maxBuffer: 2 * 1024 * 1024 });
|
|
31
32
|
ensure(verified.status === 0, `技术设计不可消费: ${verified.error?.message || verified.stderr}`);
|
|
33
|
+
if(inValidationPhase()){let output;try{output=JSON.parse(verified.stdout);}catch{throw new TypeError('VALIDATION_RECEIPT_INVALID: 技术设计子进程未返回依赖');}const dependencies=output.validation_dependencies;const required=[file,...(execution?[path.resolve(root,approvedExecutionBinding(execution).ref),path.resolve(root,approvedExecutionBinding(execution).approval_ref),path.resolve(root,'docs/agents/digital-human-roles.yaml')]:[])];ensure(required.every(ref=>dependencies?.files?.some(row=>row.file===ref)),'VALIDATION_RECEIPT_INVALID: 技术设计依赖报告缺少必要原始资产');acceptValidationDependencies(dependencies);}
|
|
32
34
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from '
|
|
1
|
+
import { readFileSync } from './validation-phase.mjs';
|
|
2
2
|
import { ensure, read, safe, hash } from './strategic-handoff-io.mjs';
|
|
3
3
|
import { validateVisualBaseline } from '../../.agents/skills/yss-prototype-stage/scripts/visual-baseline-contract.mjs';
|
|
4
4
|
import { validateExistingUiBaseline } from './existing-ui-baseline.mjs';
|