create-yss-spec 3.5.4 → 3.5.5

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.
Files changed (104) hide show
  1. package/README.md +8 -0
  2. package/cli-core.lock.json +130 -0
  3. package/package.json +7 -3
  4. package/resources/upgrade-skill/SKILL.md +26 -0
  5. package/resources/upgrade-skill/protocol.md +40 -0
  6. package/resources/upgrade-skill/references/cli-families.md +14 -0
  7. package/src/cli/help.js +1 -0
  8. package/src/cli/index.js +12 -0
  9. package/src/filesystem/transaction-runner.js +27 -0
  10. package/src/template/asset-runtime.js +6 -0
  11. package/src/template/verification-runtime.js +9 -1
  12. package/template/.agents/skills/.strategic-design-skills-manifest.json +2 -2
  13. package/template/.agents/skills/yss-harness-upgrade/SKILL.md +26 -0
  14. package/template/.agents/skills/yss-harness-upgrade/references/cli-families.md +14 -0
  15. package/template/.agents/skills/yss-implementation-contract-compiler/references/boundaries.md +1 -0
  16. package/template/.agents/skills/yss-product-lifecycle/SKILL.md +15 -19
  17. package/template/.agents/skills/yss-stage-decision/SKILL.md +3 -0
  18. package/template/.agents/skills/yss-stage-decision/scripts/validate-domain-strategy.mjs +8 -4
  19. package/template/.agents/skills/yss-stage-decision/scripts/validate-stage-decision-package.mjs +8 -4
  20. package/template/.agents/skills/yss-stage-decision/tests/run-scenarios.mjs +13 -1
  21. package/template/.codex/skills/yss-harness-upgrade/SKILL.md +26 -0
  22. package/template/.codex/skills/yss-harness-upgrade/references/cli-families.md +14 -0
  23. package/template/.codex/skills/yss-implementation-contract-compiler/references/boundaries.md +1 -0
  24. package/template/.codex/skills/yss-product-lifecycle/SKILL.md +15 -19
  25. package/template/.codex/skills/yss-stage-decision/SKILL.md +3 -0
  26. package/template/.codex/skills/yss-stage-decision/scripts/validate-domain-strategy.mjs +8 -4
  27. package/template/.codex/skills/yss-stage-decision/scripts/validate-stage-decision-package.mjs +8 -4
  28. package/template/.codex/skills/yss-stage-decision/tests/run-scenarios.mjs +13 -1
  29. package/template/.cursor/skills/yss-harness-upgrade/SKILL.md +26 -0
  30. package/template/.cursor/skills/yss-harness-upgrade/references/cli-families.md +14 -0
  31. package/template/.cursor/skills/yss-implementation-contract-compiler/references/boundaries.md +1 -0
  32. package/template/.cursor/skills/yss-product-lifecycle/SKILL.md +15 -19
  33. package/template/.cursor/skills/yss-stage-decision/SKILL.md +3 -0
  34. package/template/.cursor/skills/yss-stage-decision/scripts/validate-domain-strategy.mjs +8 -4
  35. package/template/.cursor/skills/yss-stage-decision/scripts/validate-stage-decision-package.mjs +8 -4
  36. package/template/.cursor/skills/yss-stage-decision/tests/run-scenarios.mjs +13 -1
  37. package/template/.pi/skills/yss-harness-upgrade/SKILL.md +26 -0
  38. package/template/.pi/skills/yss-harness-upgrade/references/cli-families.md +14 -0
  39. package/template/.pi/skills/yss-implementation-contract-compiler/references/boundaries.md +1 -0
  40. package/template/.pi/skills/yss-product-lifecycle/SKILL.md +15 -19
  41. package/template/.pi/skills/yss-stage-decision/SKILL.md +3 -0
  42. package/template/.pi/skills/yss-stage-decision/scripts/validate-domain-strategy.mjs +8 -4
  43. package/template/.pi/skills/yss-stage-decision/scripts/validate-stage-decision-package.mjs +8 -4
  44. package/template/.pi/skills/yss-stage-decision/tests/run-scenarios.mjs +13 -1
  45. package/template/.template-spec/agents/yss-skill-registry.yaml +9 -1
  46. package/template/.template-spec/process/contract-reading.md +1 -1
  47. package/template/.template-spec/process/document-writing.md +12 -0
  48. package/template/.template-spec/process/harness-upgrade.md +40 -0
  49. package/template/.template-spec/process/lifecycle-artifact-map.md +2 -2
  50. package/template/.template-spec/process/lifecycle-registry.yaml +3 -1
  51. package/template/.template-spec/process/schemas/approval-record.schema.json +23 -0
  52. package/template/.template-spec/process/stage-tracking.md +2 -2
  53. package/template/.template-spec/process/structured-assets.md +35 -0
  54. package/template/.template-spec/process/templates/lifecycle-checkpoint-template.json +48 -0
  55. package/template/.template-spec/templates/approval-record-template.json +23 -0
  56. package/template/scripts/check-plan-stage-coverage +12 -0
  57. package/template/scripts/contract +6 -0
  58. package/template/scripts/lib/approval-record.mjs +4 -3
  59. package/template/scripts/lib/asset-command.mjs +35 -0
  60. package/template/scripts/lib/asset-migration.mjs +101 -0
  61. package/template/scripts/lib/asset-transactions.mjs +235 -0
  62. package/template/scripts/lib/checkpoint-boundary.mjs +3 -0
  63. package/template/scripts/lib/contract-views.mjs +7 -1
  64. package/template/scripts/lib/json-schema.mjs +43 -4
  65. package/template/scripts/lib/lifecycle-context-query.mjs +7 -0
  66. package/template/scripts/lib/lifecycle-presentation.mjs +77 -0
  67. package/template/scripts/lib/lifecycle-status.mjs +16 -1
  68. package/template/scripts/lib/plan-stage-coverage.mjs +40 -0
  69. package/template/scripts/lib/reading-view-bundle.mjs +1 -0
  70. package/template/scripts/lib/reading-view-markdown.mjs +8 -6
  71. package/template/scripts/lib/stage-tracking-migration.mjs +11 -3
  72. package/template/scripts/lib/structured-assets.mjs +81 -0
  73. package/template/scripts/lib/task-package.mjs +4 -1
  74. package/template/scripts/lifecycle-status +6 -3
  75. package/template/scripts/verify-lifecycle-checkpoint +8 -5
  76. package/template/skills-lock.json +21 -9
  77. package/template.manifest.json +2 -1
  78. package/template.snapshot.json +5 -5
  79. package/vendor/cli-core/build.mjs +302 -0
  80. package/vendor/cli-core/bundle.mjs +198 -0
  81. package/vendor/cli-core/cli.mjs +182 -0
  82. package/vendor/cli-core/command-runner.mjs +107 -0
  83. package/vendor/cli-core/engine.mjs +239 -0
  84. package/vendor/cli-core/family.mjs +28 -0
  85. package/vendor/cli-core/gitignore.mjs +15 -0
  86. package/vendor/cli-core/identity.mjs +274 -0
  87. package/vendor/cli-core/io.mjs +153 -0
  88. package/vendor/cli-core/layout-migration.mjs +64 -0
  89. package/vendor/cli-core/legacy.mjs +19 -0
  90. package/vendor/cli-core/migrate.mjs +237 -0
  91. package/vendor/cli-core/migration-adapter.mjs +19 -0
  92. package/vendor/cli-core/package.json +13 -0
  93. package/vendor/cli-core/scaffold.mjs +117 -0
  94. package/vendor/cli-core/spec-migration-adapter.mjs +83 -0
  95. package/vendor/cli-core/tests/build.test.mjs +40 -0
  96. package/vendor/cli-core/tests/cli.test.mjs +1065 -0
  97. package/vendor/cli-core/tests/migration-version.test.mjs +8 -0
  98. package/vendor/cli-core/tests/package-contract.mjs +75 -0
  99. package/vendor/cli-core/transaction.mjs +529 -0
  100. package/vendor/cli-core/update.mjs +79 -0
  101. package/vendor/cli-core/vendor/NOTICES.md +6 -0
  102. package/vendor/cli-core/vendor/yaml.LICENSE +13 -0
  103. package/vendor/cli-core/vendor/yaml.mjs +6311 -0
  104. package/vendor/cli-core/verification.mjs +131 -0
@@ -0,0 +1,101 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { safeFile } from './governance-io.mjs';
4
+ import { parseAsset, readAsset, serializeAsset, byteDigest, semanticDigest, assetComments, TOOL_ROOT } from './structured-assets.mjs';
5
+ import { planAssetWrite, applyAssetWrite, assertAssetTransactionIdle } from './asset-transactions.mjs';
6
+
7
+ export function identifyAsset(value) {
8
+ if (value.stage_decision_id) return 'stage-decision-package';
9
+ if (value.domain_strategy_id) return 'domain-strategy';
10
+ if (value.task_id && value.contract && value.execution_state) return 'task-package';
11
+ if (value.gate_id && value.decision || value.kind === 'review-bundle') return 'approval-record';
12
+ if (value.repository_mode && value.stage && value.gates) return 'checkpoint';
13
+ return null;
14
+ }
15
+ const immutable = key => ['verification', 'result', 'source', 'captures', 'history', 'git_checkpoint'].includes(key);
16
+ function refs(value, out = new Set(), key = '') {
17
+ if (immutable(key)) return out;
18
+ if (typeof value === 'string' && /(?:^ref$|_ref$|_refs$|^inputs$)/.test(key) && /\.(yaml|json)$/.test(value)) out.add(value);
19
+ else if (Array.isArray(value)) value.forEach(v => refs(v, out, key));
20
+ else if (value && typeof value === 'object') for (const [name, child] of Object.entries(value)) refs(child, out, name);
21
+ return out;
22
+ }
23
+ function replaceRefs(value, mapping, key = '') {
24
+ if (immutable(key)) return structuredClone(value);
25
+ if (typeof value === 'string' && /(?:^ref$|_ref$|_refs$|^inputs$)/.test(key)) return mapping[value] || value;
26
+ if (Array.isArray(value)) return value.map(v => replaceRefs(v, mapping, key));
27
+ if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([name, child]) => [name, replaceRefs(child, mapping, name)]));
28
+ return value;
29
+ }
30
+
31
+ /** Produces reviewable candidates, never approval or a second live checkpoint. */
32
+ export function planAssetMigration(root, checkpointRef, { schemaRoot = TOOL_ROOT } = {}) {
33
+ root = fs.realpathSync(root); assertAssetTransactionIdle(root);
34
+ const queue = [checkpointRef], sources = {}, candidates = [], blockers = [], currentJson = [];
35
+ while (queue.length) {
36
+ const ref = queue.shift(); if (Object.hasOwn(sources, ref)) continue;
37
+ const file = safeFile(root, ref);
38
+ if (!fs.existsSync(file)) { blockers.push({ code: 'missing-reference', ref }); continue; }
39
+ const bytes = fs.readFileSync(file), value = parseAsset(bytes, ref), kind = identifyAsset(value);
40
+ if (!kind) continue; // Original user messages and other evidence keep their format and bytes.
41
+ sources[ref] = byteDigest(bytes);
42
+ // Completed execution packages and old reviews remain immutable evidence.
43
+ if (kind === 'task-package' && ['resolved', 'failed'].includes(value.workflow_status)) continue;
44
+ readAsset(file, kind, { schemaRoot });
45
+ queue.push(...refs(value));
46
+ if (ref.endsWith('.json')) { currentJson.push({ref,value,kind,bytes}); continue; }
47
+ const target = ref.replace(/\.yaml$/, '.json');
48
+ if (fs.existsSync(safeFile(root, target))) { blockers.push({ code: 'target-exists', ref: target }); continue; }
49
+ const comments = assetComments(bytes);
50
+ if (comments.length) blockers.push({ code: 'comment-review-required', ref, comments });
51
+ candidates.push({ source_ref: ref, ref: target, kind, source_digest: byteDigest(bytes), source_semantic_digest: semanticDigest(value), value });
52
+ }
53
+ const mapping = Object.fromEntries(candidates.map(c => [c.source_ref, c.ref]));
54
+ for (const item of currentJson) if (semanticDigest(item.value) !== semanticDigest(replaceRefs(item.value, mapping))) {
55
+ if (item.kind === 'approval-record') blockers.push({code:'current-json-reference-rebind-required',ref:item.ref,reason:'Issue a new approval record; an existing approval is immutable.'});
56
+ else candidates.push({source_ref:item.ref,ref:item.ref,kind:item.kind,source_digest:byteDigest(item.bytes),source_semantic_digest:semanticDigest(item.value),value:item.value});
57
+ }
58
+ for (const c of candidates) {
59
+ c.value = replaceRefs(c.value, mapping);
60
+ if (c.kind === 'approval-record' || ['domain-strategy', 'stage-decision-package'].includes(c.kind) && c.value.status === 'approved') {
61
+ blockers.push({ code: 'approval-rebind-required', ref: c.ref, reason: 'Candidate bytes do not inherit the source approval; issue independent current evidence.' });
62
+ }
63
+ }
64
+ // Updating digests is mechanical only for direct current bindings, not historical proof.
65
+ const byTarget = new Map(candidates.map(c => [c.ref, c]));
66
+ const ordered = candidates.sort((a,b) => ['domain-strategy','approval-record','stage-decision-package','task-package','checkpoint'].indexOf(a.kind) - ['domain-strategy','approval-record','stage-decision-package','task-package','checkpoint'].indexOf(b.kind));
67
+ function bind(value, key = '') {
68
+ if (!value || typeof value !== 'object' || immutable(key)) return;
69
+ if (value.domain_strategy_ref && byTarget.has(value.domain_strategy_ref.persisted_ref)) value.domain_strategy_ref.digest = semanticDigest(byTarget.get(value.domain_strategy_ref.persisted_ref).value);
70
+ if (typeof value.ref === 'string' && typeof value.digest === 'string' && byTarget.has(value.ref)) value.digest = byteDigest(serializeAsset(byTarget.get(value.ref).value));
71
+ for (const [name, child] of Object.entries(value)) bind(child, name);
72
+ }
73
+ for (const c of ordered) bind(c.value);
74
+ const payload = { schema_version: 1, kind: 'asset-migration-plan', root, checkpoint_ref: checkpointRef, sources, candidates: ordered, blockers, history_policy: 'preserve-original-bytes', execution_authorization: 'not-granted' };
75
+ return { ...payload, plan_id: byteDigest(serializeAsset(payload)).slice(7) };
76
+ }
77
+
78
+ /** Resolutions carry reviewed candidate objects and evidence; they do not bypass semantic validators. */
79
+ export function prepareAssetMigrationWrite(root, plan, resolutions, options = {}) {
80
+ const { plan_id, ...payload } = plan;
81
+ if (fs.realpathSync(root) !== plan.root || plan.kind !== 'asset-migration-plan' || plan_id !== byteDigest(serializeAsset(payload)).slice(7)) throw new Error('ASSET_MIGRATION_PLAN_INVALID');
82
+ for (const [ref, digest] of Object.entries(plan.sources)) if (byteDigest(fs.readFileSync(safeFile(root, ref))) !== digest) throw new Error(`ASSET_MIGRATION_DRIFT: ${ref}`);
83
+ if (plan.blockers.some(b => ['missing-reference', 'target-exists', 'current-json-reference-rebind-required'].includes(b.code))) throw new Error('ASSET_MIGRATION_BLOCKED: rebuild the plan after resolving missing/conflicting inputs');
84
+ if (plan.blockers.length && (!resolutions || resolutions.plan_id !== plan_id)) throw new Error('ASSET_MIGRATION_REVIEW_REQUIRED');
85
+ for (const b of plan.blockers) {
86
+ const resolution = resolutions.items?.find(r => r.ref === b.ref && r.code === b.code);
87
+ if (!resolution?.evidence_ref || !resolution.evidence_digest || byteDigest(fs.readFileSync(safeFile(root, resolution.evidence_ref))) !== resolution.evidence_digest) throw new Error(`ASSET_MIGRATION_EVIDENCE_REQUIRED: ${b.ref}`);
88
+ }
89
+ const replacements = resolutions?.candidates || [];
90
+ for (const replacement of replacements) if (!plan.candidates.some(c => c.ref === replacement.ref && c.kind === replacement.kind)) throw new Error('ASSET_MIGRATION_SCOPE_CHANGED');
91
+ const specs = plan.candidates.map(c => replacements.find(r => r.ref === c.ref) || c);
92
+ const migration={plan_id:plan.plan_id,sources:plan.sources,source_to_target:Object.fromEntries(plan.candidates.filter(c=>c.source_ref!==c.ref).map(c=>[c.source_ref,c.ref]))};
93
+ return planAssetWrite(root, specs, {...options,migration});
94
+ }
95
+
96
+ export function applyAssetMigration(root, plan, resolutions, options = {}) {
97
+ const writePlan = prepareAssetMigrationWrite(root, plan, resolutions, options);
98
+ const result = applyAssetWrite(root, writePlan, options);
99
+ const updated = new Set(writePlan.changes.filter(c => Object.hasOwn(plan.sources,c.ref)).map(c => c.ref));
100
+ return { ...result, migration_plan_id: plan.plan_id, preserved_sources: Object.keys(plan.sources).filter(ref => !updated.has(ref)), rebound_json_sources: [...updated] };
101
+ }
@@ -0,0 +1,235 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { randomUUID } from 'node:crypto';
6
+ import { safeFile } from './governance-io.mjs';
7
+ import { parseAsset, readAsset, serializeAsset, byteDigest, semanticDigest, validateAssetStructure, TOOL_ROOT } from './structured-assets.mjs';
8
+
9
+ const TX = '.yss/asset-transactions';
10
+ const ensure = (ok, message) => { if (!ok) throw new Error(message); };
11
+ const exists = file => fs.existsSync(file);
12
+ const descriptor = file => exists(file) ? { digest: byteDigest(fs.readFileSync(file)), mode: fs.statSync(file).mode & 0o777 } : null;
13
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
14
+
15
+ /** Adapter for an existing validated writer which also owns Markdown/tracker updates. */
16
+ export function runAssetTransaction(root, changes, action) {
17
+ root=fs.realpathSync(root);assertAssetTransactionIdle(root);
18
+ const directory=safeFile(root,TX);fs.mkdirSync(directory,{recursive:true});
19
+ const lock=safeFile(root,`${TX}/lock`),fd=fs.openSync(lock,'wx');fs.writeFileSync(fd,String(process.pid));fs.fsyncSync(fd);
20
+ const plan_id=byteDigest(serializeAsset({changes,nonce:randomUUID()})).slice(7),journal_ref=`${TX}/${plan_id}.json`,active=safeFile(root,`${TX}/active.json`);
21
+ const tx={schema_version:1,root,plan_id,status:'applying',changes,backups:{}};
22
+ try {
23
+ ensure(!exists(safeFile(root, `${TX}/recovery.lock`)), 'ASSET_RECOVERY_LOCKED');
24
+ for(const change of changes){ensure(same(descriptor(safeFile(root,change.ref)),change.before),`ASSET_INPUT_DRIFT: ${change.ref}`);tx.backups[change.ref]=change.before?fs.readFileSync(safeFile(root,change.ref)).toString('base64'):null;}
25
+ atomic(safeFile(root,journal_ref),serializeAsset(tx));atomic(active,serializeAsset({journal_ref}));
26
+ const result=action();
27
+ for(const change of changes)ensure(descriptor(safeFile(root,change.ref))?.digest===change.after_digest,`ASSET_POSTWRITE_DRIFT: ${change.ref}`);
28
+ tx.status='applied';atomic(safeFile(root,journal_ref),serializeAsset(tx));fs.unlinkSync(active);return result;
29
+ } catch(error) {
30
+ if(exists(active)){const recovered=recoverAssetWrite(root,{lockHeld:true});throw new Error(`${error.message}; ${recovered.status}`);}
31
+ throw error;
32
+ } finally {fs.closeSync(fd);if(exists(lock))fs.unlinkSync(lock);}
33
+ }
34
+
35
+ export function assertAssetTransactionIdle(root) {
36
+ ensure(!exists(safeFile(root, `${TX}/active.json`)), 'ASSET_TRANSACTION_PENDING: recover the unfinished transaction before lifecycle transition');
37
+ }
38
+
39
+ export function assertCurrentAssetReference(root, ref) {
40
+ const directory = safeFile(root, TX);
41
+ if (!exists(directory)) return;
42
+ for (const name of fs.readdirSync(directory)) {
43
+ if (!/^[a-f0-9]{64}\.json$/.test(name)) continue;
44
+ const receipt = parseAsset(fs.readFileSync(path.join(directory, name)), name);
45
+ if (receipt.status === 'applied' && receipt.migration?.source_to_target?.[ref]) throw new Error(`ASSET_HISTORICAL_ONLY: ${ref}; current=${receipt.migration.source_to_target[ref]}`);
46
+ }
47
+ }
48
+
49
+ // The observed closure includes governance, tools and project evidence, but not external checkouts/caches.
50
+ export function observeAssetInputs(root) {
51
+ const rows = {};
52
+ function walk(ref) {
53
+ const file = safeFile(root, ref);
54
+ if (!exists(file)) return;
55
+ const st = fs.lstatSync(file);
56
+ ensure(!st.isSymbolicLink(), `ASSET_SYMLINK: ${ref}`);
57
+ if (st.isDirectory()) for (const name of fs.readdirSync(file).sort()) {
58
+ if (['node_modules', '.git', '.codegraph', '__pycache__'].includes(name)) continue;
59
+ walk(`${ref}/${name}`);
60
+ }
61
+ else if (st.isFile()) rows[ref] = descriptor(file);
62
+ }
63
+ for (const ref of ['yss-project.yaml', 'CONTEXT.md', 'DESIGN.md', 'AGENTS.md', 'skills-lock.json', '.template-spec', '.agents', 'scripts', 'docs']) walk(ref);
64
+ return rows;
65
+ }
66
+
67
+ export function validateAssetSemantics(root, ref, kind) {
68
+ const file = safeFile(root, ref), { value } = readAsset(file, kind, { schemaRoot: root });
69
+ let tool, args;
70
+ if (kind === 'domain-strategy' || kind === 'stage-decision-package') {
71
+ tool = `.agents/skills/yss-stage-decision/scripts/validate-${kind}.mjs`;
72
+ args = [file, '--root', root];
73
+ } else {
74
+ tool = { checkpoint: 'scripts/verify-lifecycle-checkpoint', 'task-package': 'scripts/verify-digital-human-task-package', 'approval-record': 'scripts/verify-approval-record' }[kind];
75
+ args = kind === 'approval-record' ? [value.decision === 'approved' || value.kind === 'review-bundle' ? '--require-approved' : '--history', file] : [file];
76
+ }
77
+ ensure(exists(safeFile(root, tool)), `ASSET_CONSUMER_MISSING: ${tool}`);
78
+ const result = spawnSync(process.execPath, [path.join(root, tool), ...args], { cwd: root, encoding: 'utf8', timeout: 60000, maxBuffer: 8 * 1024 * 1024 });
79
+ ensure(!result.error && result.status === 0, `ASSET_SEMANTIC: ${ref}: ${result.error?.message || result.stderr || result.stdout}`);
80
+ return { ref, kind, command: [tool, ...args.map(x => x.replaceAll(root, '.'))], exit_code: result.status, output: result.stdout.trim() };
81
+ }
82
+
83
+ function approvalBindings(root, changes) {
84
+ for (const change of changes) {
85
+ const value = parseAsset(change.after, change.ref);
86
+ // Writers never turn a bare status into an approval. Bound, independently issued evidence is mandatory.
87
+ const records = change.kind === 'approval-record' ? value.kind === 'review-bundle' ? value.reviews : [value] : [];
88
+ for (const row of records) if (row.decision === 'approved') {
89
+ ensure(row.subject_ref && row.subject_digest && row.drafter_principal_ref && row.drafter_principal_ref !== row.principal_ref, 'ASSET_APPROVAL_REBIND_REQUIRED: subject and independent reviewer required');
90
+ ensure(byteDigest(fs.readFileSync(safeFile(root, row.subject_ref))).replace('sha256:', '') === row.subject_digest.replace('sha256:', ''), 'ASSET_APPROVAL_STALE');
91
+ }
92
+ if (['domain-strategy', 'stage-decision-package'].includes(change.kind) && value.status === 'approved') {
93
+ const approval = parseAsset(fs.readFileSync(safeFile(root, value.approval.approval_ref)), value.approval.approval_ref);
94
+ const check = change.kind === 'domain-strategy' ? 'check.domain-strategy-approved' : 'check.stage-decision-package-approved';
95
+ const row = approval.kind === 'review-bundle' ? approval.reviews.find(x => x.gate_id === check) : approval;
96
+ ensure(row?.subject_ref === change.ref && row.subject_digest?.replace('sha256:', '') === byteDigest(change.after).replace('sha256:', ''), `ASSET_APPROVAL_REBIND_REQUIRED: ${change.ref}`);
97
+ }
98
+ }
99
+ }
100
+
101
+ function overlayVerification(root, observed, changes, { verify = validateAssetSemantics } = {}) {
102
+ const staging = fs.mkdtempSync(path.join(os.tmpdir(), 'yss-asset-check-'));
103
+ try {
104
+ for (const ref of Object.keys(observed)) {
105
+ const target = safeFile(staging, ref); fs.mkdirSync(path.dirname(target), { recursive: true });
106
+ fs.copyFileSync(safeFile(root, ref), target); fs.chmodSync(target, observed[ref].mode);
107
+ }
108
+ for (const change of changes) {
109
+ const target = safeFile(staging, change.ref); fs.mkdirSync(path.dirname(target), { recursive: true }); fs.writeFileSync(target, change.after);
110
+ }
111
+ approvalBindings(staging, changes);
112
+ return changes.map(change => verify(staging, change.ref, change.kind));
113
+ } finally { fs.rmSync(staging, { recursive: true, force: true }); }
114
+ }
115
+
116
+ export function planAssetWrite(root, specs, { schemaRoot = TOOL_ROOT, verify, migration } = {}) {
117
+ root = fs.realpathSync(root); assertAssetTransactionIdle(root);
118
+ ensure(Array.isArray(specs) && specs.length, 'ASSET_EMPTY_PLAN');
119
+ const observed = observeAssetInputs(root), seen = new Set();
120
+ const changes = specs.map(({ ref, kind, value }) => {
121
+ ensure(!seen.has(ref), `ASSET_DUPLICATE_TARGET: ${ref}`); seen.add(ref);
122
+ ensure(ref.endsWith('.json') && /^(docs\/|\.yss\/)/.test(ref) && !ref.startsWith(`${TX}/`), 'ASSET_WRITE_SCOPE: project JSON assets only');
123
+ const file = safeFile(root, ref), before = descriptor(file);
124
+ const legacy = ref.replace(/\.json$/, '.yaml');
125
+ if (!before && exists(safeFile(root, legacy))) ensure(migration?.source_to_target?.[legacy] === ref, 'ASSET_AMBIGUOUS_AUTHORITY: use plan-migrate');
126
+ if (kind === 'approval-record' && before) throw new Error('ASSET_IMMUTABLE_APPROVAL: issue a new record');
127
+ validateAssetStructure(value, kind, { schemaRoot });
128
+ const after = serializeAsset(value); parseAsset(after, ref);
129
+ return { ref, kind, before, after, after_digest: byteDigest(after), semantic_digest: semanticDigest(value) };
130
+ }).filter(change => change.before?.digest !== change.after_digest);
131
+ for (const change of changes) if (!Object.hasOwn(observed, change.ref)) observed[change.ref] = change.before;
132
+ const verification = changes.length ? overlayVerification(root, Object.fromEntries(Object.entries(observed).filter(([,v]) => v)), changes, { verify }) : [];
133
+ const payload = { schema_version: 1, kind: 'asset-write-plan', root, observed, changes, verification, ...(migration ? {migration} : {}), execution_authorization: 'not-granted' };
134
+ return { ...payload, plan_id: byteDigest(serializeAsset(payload)).slice(7) };
135
+ }
136
+
137
+ function atomic(file, bytes, mode = 0o600) {
138
+ fs.mkdirSync(path.dirname(file), { recursive: true });
139
+ const temp = `${file}.${randomUUID()}.tmp`;
140
+ const fd = fs.openSync(temp, 'wx', mode);
141
+ try { fs.writeFileSync(fd, bytes); fs.fsyncSync(fd); } finally { fs.closeSync(fd); }
142
+ try { ensure(fs.readFileSync(temp).equals(Buffer.from(bytes)), 'ASSET_TEMP_READBACK'); fs.renameSync(temp, file); const dir = fs.openSync(path.dirname(file), 'r'); try { fs.fsyncSync(dir); } finally { fs.closeSync(dir); } }
143
+ finally { if (exists(temp)) fs.unlinkSync(temp); }
144
+ }
145
+
146
+ export function applyAssetWrite(root, plan, { verify, afterWrite } = {}) {
147
+ root = fs.realpathSync(root); assertAssetTransactionIdle(root);
148
+ const { plan_id, ...payload } = plan;
149
+ ensure(plan.root === root && plan.kind === 'asset-write-plan' && plan_id === byteDigest(serializeAsset(payload)).slice(7), 'ASSET_PLAN_INVALID');
150
+ const directory = safeFile(root, TX); fs.mkdirSync(directory, { recursive: true });
151
+ const lock = safeFile(root, `${TX}/lock`), fd = fs.openSync(lock, 'wx');
152
+ fs.writeFileSync(fd, String(process.pid));fs.fsyncSync(fd);
153
+ const active = safeFile(root, `${TX}/active.json`);
154
+ let transaction;
155
+ try {
156
+ ensure(!exists(safeFile(root, `${TX}/recovery.lock`)), 'ASSET_RECOVERY_LOCKED');
157
+ const current = observeAssetInputs(root);
158
+ for (const [ref, before] of Object.entries(plan.observed)) ensure(same(descriptor(safeFile(root, ref)), before), `ASSET_INPUT_DRIFT: ${ref}`);
159
+ ensure(Object.keys(current).every(ref => Object.hasOwn(plan.observed, ref)), 'ASSET_INPUT_DRIFT: new dependency files');
160
+ if (!plan.changes.length) return { status: 'unchanged', plan_id, changed_refs: [], execution_authorization: 'not-granted' };
161
+ // Recompute, including schemas and semantics. A plan's recorded successful exit is never trusted.
162
+ if(plan.migration) for(const [ref,digest] of Object.entries(plan.migration.sources)) ensure(byteDigest(fs.readFileSync(safeFile(root,ref)))===digest,`ASSET_MIGRATION_DRIFT: ${ref}`);
163
+ const fresh = planAssetWrite(root, plan.changes.map(c => ({ ref: c.ref, kind: c.kind, value: parseAsset(c.after, c.ref) })), { schemaRoot: root, verify, migration:plan.migration });
164
+ ensure(same(fresh.changes, plan.changes), 'ASSET_PLAN_CHANGED');
165
+ const journalRef = `${TX}/${plan_id}.json`, journalFile = safeFile(root, journalRef);
166
+ ensure(!exists(journalFile), 'ASSET_ALREADY_APPLIED: inspect the existing receipt');
167
+ transaction = { schema_version: 1, plan_id, root, status: 'applying', ...(plan.migration?{migration:plan.migration}:{}), changes: plan.changes, backups: Object.fromEntries(plan.changes.map(c => [c.ref, c.before ? fs.readFileSync(safeFile(root, c.ref)).toString('base64') : null])) };
168
+ atomic(journalFile, serializeAsset(transaction)); atomic(active, serializeAsset({ journal_ref: journalRef }));
169
+ for (const change of plan.changes) {
170
+ ensure(same(descriptor(safeFile(root, change.ref)), change.before), `ASSET_INPUT_DRIFT: ${change.ref}`);
171
+ atomic(safeFile(root, change.ref), change.after, change.before?.mode ?? 0o644);
172
+ readAsset(safeFile(root, change.ref), change.kind, { schemaRoot: root });
173
+ afterWrite?.(change);
174
+ }
175
+ for (const [ref, before] of Object.entries(plan.observed)) {
176
+ const change = plan.changes.find(c => c.ref === ref);
177
+ ensure(descriptor(safeFile(root, ref))?.digest === (change?.after_digest ?? before?.digest), `ASSET_POSTWRITE_DRIFT: ${ref}`);
178
+ }
179
+ ensure(Object.keys(observeAssetInputs(root)).every(ref => Object.hasOwn(plan.observed, ref)), 'ASSET_POSTWRITE_DRIFT: new dependency files');
180
+ transaction.status = 'applied'; atomic(journalFile, serializeAsset(transaction)); fs.unlinkSync(active);
181
+ return { status: 'applied', plan_id, changed_refs: plan.changes.map(c => c.ref), receipt_ref: journalRef, execution_authorization: 'not-granted' };
182
+ } catch (error) {
183
+ if (transaction && exists(active)) {
184
+ const result = recoverAssetWrite(root, { lockHeld: true });
185
+ throw new Error(`${error.message}; ${result.status}: ${result.conflicts.join(',')}`);
186
+ }
187
+ throw error;
188
+ } finally { fs.closeSync(fd); if (exists(lock)) fs.unlinkSync(lock); }
189
+ }
190
+
191
+ export function recoverAssetWrite(root, { lockHeld = false, recoveryHeld = false } = {}) {
192
+ root = fs.realpathSync(root);
193
+ const lock = safeFile(root, `${TX}/lock`);
194
+ if (!lockHeld && !recoveryHeld) {
195
+ const guard = safeFile(root, `${TX}/recovery.lock`);
196
+ fs.mkdirSync(path.dirname(guard), { recursive: true });
197
+ const fd = fs.openSync(guard, 'wx');
198
+ try {
199
+ fs.writeFileSync(fd, String(process.pid)); fs.fsyncSync(fd);
200
+ return recoverAssetWrite(root, { recoveryHeld: true });
201
+ } finally { fs.closeSync(fd); fs.unlinkSync(guard); }
202
+ }
203
+ if (!lockHeld && exists(lock)) {
204
+ const pid=Number(fs.readFileSync(lock,'utf8'));ensure(Number.isInteger(pid)&&pid>0,'ASSET_LOCK_INVALID');
205
+ let live=true;try{process.kill(pid,0);}catch(error){if(error.code==='ESRCH')live=false;else throw error;}
206
+ ensure(!live,'ASSET_LOCKED: writer is still alive');fs.unlinkSync(lock);
207
+ }
208
+ if (!lockHeld) {
209
+ fs.mkdirSync(path.dirname(lock), { recursive: true });
210
+ const fd = fs.openSync(lock, 'wx');
211
+ try {
212
+ fs.writeFileSync(fd, String(process.pid)); fs.fsyncSync(fd);
213
+ return recoverAssetWrite(root, { lockHeld: true });
214
+ } finally { fs.closeSync(fd); fs.unlinkSync(lock); }
215
+ }
216
+ const active = safeFile(root, `${TX}/active.json`);
217
+ if (!exists(active)) return { status: 'idle', conflicts: [] };
218
+ const marker = parseAsset(fs.readFileSync(active), active), journalFile = safeFile(root, marker.journal_ref);
219
+ ensure(marker.journal_ref.startsWith(`${TX}/`), 'ASSET_JOURNAL_PATH');
220
+ const tx = parseAsset(fs.readFileSync(journalFile), journalFile);
221
+ ensure(tx.root === root && ['applying', 'recovery-conflict','applied'].includes(tx.status), 'ASSET_JOURNAL_INVALID');
222
+ if(tx.status==='applied') {for(const c of tx.changes)ensure(descriptor(safeFile(root,c.ref))?.digest===c.after_digest,'ASSET_RECOVERY_CONFLICT');fs.unlinkSync(active);return{status:'applied',conflicts:[]};}
223
+ const conflicts = [];
224
+ for (const change of [...tx.changes].reverse()) {
225
+ ensure(byteDigest(change.after)===change.after_digest && (change.before===null?tx.backups[change.ref]===null:byteDigest(Buffer.from(tx.backups[change.ref],'base64'))===change.before.digest),'ASSET_JOURNAL_CORRUPT');
226
+ const file = safeFile(root, change.ref), actual = descriptor(file);
227
+ if (same(actual, change.before)) continue;
228
+ if (actual?.digest !== change.after_digest) { conflicts.push(change.ref); continue; }
229
+ if (tx.backups[change.ref] === null) fs.unlinkSync(file);
230
+ else atomic(file, Buffer.from(tx.backups[change.ref], 'base64'), change.before.mode);
231
+ }
232
+ tx.status = conflicts.length ? 'recovery-conflict' : 'rolled-back'; tx.conflicts = conflicts;
233
+ atomic(journalFile, serializeAsset(tx)); if (!conflicts.length) fs.unlinkSync(active);
234
+ return { status: tx.status, conflicts };
235
+ }
@@ -1,8 +1,11 @@
1
1
  import { readDocument, fileBinding } from './governance-io.mjs';
2
+ import { assertAssetTransactionIdle, assertCurrentAssetReference } from './asset-transactions.mjs';
2
3
 
3
4
  /** Validate existing lifecycle context evidence; never grants approval. */
4
5
  export function assertCheckpointBoundary(value, {root, history=false}={}) {
5
6
  if(history) return {status:'historical-only',execution_authorization:'not-evaluated'};
7
+ assertAssetTransactionIdle(root);
8
+ if(value.stage_tracking?.checkpoint_ref)assertCurrentAssetReference(root,value.stage_tracking.checkpoint_ref);
6
9
  const canonical='.agents/skills/yss-product-lifecycle/references/orchestration-contract.yaml';
7
10
  const contract=readDocument(root,fileBinding(root,canonical)?canonical:'.template-spec/process/checkpoint-boundary.yaml');
8
11
  const registry=readDocument(root,'.template-spec/process/lifecycle-registry.yaml');
@@ -4,6 +4,7 @@ import {safe,hash,schema} from './strategic-handoff-io.mjs';
4
4
  import {parseSliceYaml,readSliceContract} from './slice-contract.mjs';
5
5
  import {renderSliceContractView,diffSliceContracts} from './slice-contract-views.mjs';
6
6
  import {renderReadingMarkdown} from './reading-view-markdown.mjs';
7
+ import {loadLifecyclePresenter} from './lifecycle-presentation.mjs';
7
8
  import {readingDiff,renderReadingDiff} from './reading-view-diff.mjs';
8
9
  import {adaptReading} from './reading-view-adapters.mjs';
9
10
  import {readingViewModel} from './reading-view-model.mjs';
@@ -120,7 +121,12 @@ function view(ref,{root=process.cwd(),kind,profile='review',unit_id,task_layout=
120
121
  content.约束分类='保留未分类字段;本视图只允许阅读,执行影响由资产所有者核验。';
121
122
  }
122
123
  }
123
- const view={read_only:true,execution_allowed:false,kind,profile,binding,checks,approval_validity:'not-checked',content,blockers};if(include_reading_model&&['domain-strategy','stage-decision-package','checkpoint','tracking-migration'].includes(kind))view.reading_model=readingViewModel(view,sourceRaw);view.markdown=markdown(view);return view;
124
+ const view={read_only:true,execution_allowed:false,kind,profile,binding,checks,approval_validity:'not-checked',content,blockers};
125
+ if(['domain-strategy','stage-decision-package','checkpoint'].includes(kind)) {
126
+ const presenter=loadLifecyclePresenter(root);
127
+ view.presentation={names:presenter.catalog(content),sources:presenter.sources,warnings:presenter.warnings};
128
+ }
129
+ if(include_reading_model&&['domain-strategy','stage-decision-package','checkpoint','tracking-migration'].includes(kind))view.reading_model=readingViewModel(view,sourceRaw);view.markdown=markdown(view);return view;
124
130
  }
125
131
  export function diffContract(beforeRef,afterRef,{root=process.cwd(),beforeRoot=root,kind}={}){
126
132
  validateKind(kind);if(kind==='slice')return diffSliceContracts(beforeRef,afterRef,{root,beforeRoot});
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync, validationCache } from './validation-phase.mjs';
2
2
  import { spawnSync } from "node:child_process";
3
3
  import path from "node:path";
4
+ import { pathToFileURL } from 'node:url';
4
5
 
5
6
  const schemaCache=Symbol('schema-results');
6
7
  const validator = String.raw`
@@ -8,6 +9,8 @@ import json
8
9
  import sys
9
10
  import traceback
10
11
  from jsonschema import Draft202012Validator, FormatChecker
12
+ from referencing import Registry, Resource
13
+ from referencing.jsonschema import DRAFT202012
11
14
  validators = {}
12
15
  results = []
13
16
  expected_bytes = int(sys.argv[1])
@@ -23,12 +26,18 @@ for item in jobs:
23
26
  key = (item['schemaPath'], item['formatChecker'])
24
27
  if key not in validators:
25
28
  schema = json.loads(item['schemaText'])
26
- validators[key] = Draft202012Validator(schema, format_checker=FormatChecker() if key[1] else None)
29
+ Draft202012Validator.check_schema(schema)
30
+ registry = Registry()
31
+ for resource in item.get('resources', []):
32
+ contents = json.loads(resource['text'])
33
+ Draft202012Validator.check_schema(contents)
34
+ registry = registry.with_resource(resource['uri'], Resource.from_contents(contents, default_specification=DRAFT202012))
35
+ validators[key] = Draft202012Validator(schema, registry=registry, format_checker=FormatChecker() if key[1] else None)
27
36
  errors = list(validators[key].iter_errors(item['value']))
28
37
  if item['errorStyle'] == 'verbose':
29
38
  message = '\n'.join(str(error) for error in errors)
30
39
  else:
31
- errors.sort(key=lambda error: list(error.absolute_path))
40
+ errors.sort(key=lambda error: [str(part) for part in error.absolute_path])
32
41
  message = '\n'.join(f"{'.'.join(str(part) for part in error.absolute_path) or '<root>'}: {error.message}" for error in errors)
33
42
  results.append({'valid': not errors, 'error': message})
34
43
  except Exception:
@@ -36,6 +45,36 @@ for item in jobs:
36
45
  json.dump(results, sys.stdout)
37
46
  `;
38
47
 
48
+ // Supply the complete local reference closure; never retrieve schemas over the network.
49
+ function schemaResources(schemaPath) {
50
+ const resources = new Map(), visited = new Set();
51
+ function load(file, uri) {
52
+ const text = readFileSync(file, 'utf8'), schema = JSON.parse(text);
53
+ const fileURI = pathToFileURL(file).href;
54
+ for (const id of [uri, fileURI, schema.$id].filter(Boolean)) {
55
+ if (resources.has(id) && resources.get(id) !== text) throw new TypeError(`JSON_SCHEMA_ID_CONFLICT: ${id}`);
56
+ resources.set(id, text);
57
+ }
58
+ if (visited.has(file)) return;
59
+ visited.add(file);
60
+ const base = schema.$id || uri || fileURI;
61
+ function refs(value) {
62
+ if (!value || typeof value !== 'object') return;
63
+ for (const [key, ref] of Object.entries(value)) {
64
+ if (['$ref', '$dynamicRef'].includes(key) && typeof ref === 'string' && !ref.startsWith('#')) {
65
+ const resource = ref.split('#')[0], resolved = new URL(resource, base).href;
66
+ if (resources.has(resolved)) continue;
67
+ if (/^[a-z][a-z0-9+.-]*:/i.test(resource) || path.isAbsolute(resource)) throw new TypeError(`JSON_SCHEMA_OFFLINE: unregistered schema ${ref}`);
68
+ load(path.resolve(path.dirname(file), resource), resolved);
69
+ } else refs(ref);
70
+ }
71
+ }
72
+ refs(schema);
73
+ }
74
+ load(schemaPath);
75
+ return [...resources].map(([uri, text]) => ({ uri, text }));
76
+ }
77
+
39
78
  /** One immutable validation phase, never a cache of successful work units. */
40
79
  export function validateJsonSchemas(items, { cwd = process.cwd(), timeoutMs = 30000 } = {}) {
41
80
  if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) throw new TypeError("schema timeoutMs 必须是正整数毫秒数");
@@ -45,7 +84,7 @@ export function validateJsonSchemas(items, { cwd = process.cwd(), timeoutMs = 30
45
84
  const effectiveSchema = path.resolve(schemaPath);
46
85
  if (!existsSync(effectiveSchema)) throw new TypeError(`缺少 ${label}: ${effectiveSchema}`);
47
86
  if (!["compact", "verbose"].includes(errorStyle)) throw new TypeError(`未知 schema errorStyle: ${errorStyle}`);
48
- return { value, schemaPath: effectiveSchema, schemaText: readFileSync(effectiveSchema, 'utf8'), formatChecker, errorStyle };
87
+ return { value, schemaPath: effectiveSchema, schemaText: readFileSync(effectiveSchema, 'utf8'), resources: schemaResources(effectiveSchema), formatChecker, errorStyle };
49
88
  });
50
89
  // readall waits for EOF even after the complete JSON has arrived. A known UTF-8
51
90
  // byte frame lets the validator finish when a transport keeps its write end open.
@@ -62,7 +101,7 @@ export function validateJsonSchemas(items, { cwd = process.cwd(), timeoutMs = 30
62
101
  };
63
102
  // Batch identical jobs once and reuse only deterministic checks within this operation.
64
103
  // External references retain the uncached path: their dependency closure is not local here.
65
- const keys=jobs.map(job=>JSON.stringify([job.schemaPath,job.schemaText,job.value,job.formatChecker,job.errorStyle,path.resolve(cwd),timeoutMs]));
104
+ const keys=jobs.map(job=>JSON.stringify([job.schemaPath,job.schemaText,job.resources,job.value,job.formatChecker,job.errorStyle,path.resolve(cwd),timeoutMs]));
66
105
  const pending=new Map(),cached=new Map();
67
106
  for(let i=0;i<jobs.length;i++){
68
107
  if(/"\$(?:ref|dynamicRef)"\s*:\s*"(?!#)/.test(jobs[i].schemaText)){pending.set(`${i}:${keys[i]}`,{job:jobs[i],indices:[i],cache:false});continue;}
@@ -5,6 +5,7 @@ import path from "node:path";
5
5
  import { parseDocument } from "../vendor/yaml.mjs";
6
6
  import { ROOT, loadRegistry, semanticDigest, validateRegistry } from "./lifecycle-registry.mjs";
7
7
  import { loadSkillRegistry } from "./skill-registry.mjs";
8
+ import { loadLifecyclePresenter } from './lifecycle-presentation.mjs';
8
9
 
9
10
  const LIFECYCLE_REGISTRY_REF = ".template-spec/process/lifecycle-registry.yaml";
10
11
  const ORCHESTRATION_CONTRACT_REF = ".agents/skills/yss-product-lifecycle/references/orchestration-contract.yaml";
@@ -159,6 +160,12 @@ function querySnapshot({ mode, stageId, workUnitId, include = [] } = {}) {
159
160
  },
160
161
  skills: selectedSkills(route, skillRegistry),
161
162
  };
163
+ const presenter = loadLifecyclePresenter(ROOT);
164
+ result.presentation = { read_only: true, execution_allowed: false, approval_validity: 'not-checked',
165
+ names: presenter.catalog(result.lifecycle), sources: presenter.sources, warnings: presenter.warnings };
166
+ // Include role and transition references from the requested contract subtrees as well.
167
+ Object.assign(result.presentation.names, presenter.catalog(result.execution));
168
+ Object.assign(result.presentation.names, presenter.catalog(presenter.roleIds));
162
169
  result.references = [...collectPathReferences(result), LIFECYCLE_REGISTRY_REF, ORCHESTRATION_CONTRACT_REF, SKILL_REGISTRY_REF]
163
170
  .filter((value, index, all) => all.indexOf(value) === index)
164
171
  .sort();
@@ -0,0 +1,77 @@
1
+ import path from 'node:path';
2
+ import {createHash} from 'node:crypto';
3
+ import {readFileSync} from './validation-phase.mjs';
4
+ import {parseDocument} from '../vendor/yaml.mjs';
5
+ import {safe} from './strategic-handoff-io.mjs';
6
+
7
+ const collections = ['stages', 'gates', 'checks', 'artifacts', 'work_units', 'evidence'];
8
+ const token = /(?<![\w./-])(?:stage|gate|check|artifact|work-unit|evidence|role)\.[a-z0-9][a-z0-9-]*(?![\w./-])/g;
9
+ export const presentationRefs = {
10
+ registry: '.template-spec/process/lifecycle-registry.yaml',
11
+ roles: '.template-spec/agents/digital-human-roles.yaml',
12
+ };
13
+
14
+ /** Names describe identities, never approval, readiness or execution authority. */
15
+ export function createLifecyclePresenter(registry = {}, roles = {}) {
16
+ const records = [...collections.flatMap(key => registry[key] ?? []), ...(roles.roles ?? []), ...(roles.orchestrator ? [roles.orchestrator] : [])];
17
+ const byId = new Map(records.filter(record => record && typeof record.id === 'string').map(record => [record.id, record]));
18
+ const describe = id => {
19
+ const record = byId.get(id);
20
+ const name = record?.public_name ?? record?.name;
21
+ return {id, name: typeof name === 'string' && name.trim() ? name : '名称未识别', known: typeof name === 'string' && !!name.trim()};
22
+ };
23
+ const label = (id, {trace = true} = {}) => {
24
+ const item = describe(id);
25
+ return item.known && !trace ? item.name : `${item.name}(${id})`;
26
+ };
27
+ const text = (value, options) => String(value).replace(token, id => label(id, options));
28
+ const catalog = value => {
29
+ const ids = new Set();
30
+ const visit = item => {
31
+ if (typeof item === 'string') for (const id of item.match(token) ?? []) ids.add(id);
32
+ else if (item && typeof item === 'object') for (const [key, child] of Object.entries(item)) { visit(key); visit(child); }
33
+ };
34
+ visit(value);
35
+ return Object.fromEntries([...ids].sort().map(id => [id, describe(id)]));
36
+ };
37
+ return {describe, label, text, catalog, roleIds: records.filter(record => typeof record?.id === 'string' && record.id.startsWith('role.')).map(record => record.id)};
38
+ }
39
+
40
+ /** Optional presentation sources: legacy instances remain readable when names are unavailable. */
41
+ export function loadLifecyclePresenter(root) {
42
+ const values = {}, sources = [], warnings = [];
43
+ for (const [key, ref] of Object.entries(presentationRefs)) {
44
+ let digest = null;
45
+ try {
46
+ const bytes = readFileSync(safe(path.resolve(root), ref));
47
+ digest = `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
48
+ const doc = parseDocument(String(bytes), {uniqueKeys: true, maxAliasCount: 0});
49
+ if (doc.errors.length) throw Error(doc.errors[0].message);
50
+ const value = doc.toJS({maxAliasCount: 0});
51
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw Error('名称来源须为对象');
52
+ for (const collection of key === 'registry' ? collections : ['roles']) {
53
+ if (value[collection] !== undefined && !Array.isArray(value[collection])) throw Error(`${collection} 须为数组`);
54
+ }
55
+ values[key] = value;
56
+ } catch (error) {
57
+ warnings.push(`名称来源不可用:${ref};${error.message}`);
58
+ }
59
+ sources.push({ref, digest});
60
+ }
61
+ return {...createLifecyclePresenter(values.registry, values.roles), sources, warnings};
62
+ }
63
+
64
+ export function renderLifecycleStatus(result) {
65
+ const view = result.presentation;
66
+ if (!view) throw Error('缺少状态阅读信息');
67
+ return [
68
+ `当前阶段:${view.stage}。登记状态:${view.checkpoint_status}。`,
69
+ `当前阻塞:${view.blockers.length ? view.blockers.join(';') : '未登记阻塞;完整就绪与批准仍待核验'}。`,
70
+ `负责人:${view.owner}。`,
71
+ `下一项工作:${view.work_unit}。`,
72
+ ...(view.expected_output ? [`预期产出:${view.expected_output}`] : []),
73
+ `下一动作:${view.next_action}。`,
74
+ ...view.warnings,
75
+ '本次为只读查询,未核验完整批准或执行授权。',
76
+ ].join('\n') + '\n';
77
+ }
@@ -6,6 +6,7 @@ import { readFileSync } from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import { parseDocument } from '../vendor/yaml.mjs';
8
8
  import { safeTrackingPath, trackingDrift } from './stage-tracking.mjs';
9
+ import { loadLifecyclePresenter } from './lifecycle-presentation.mjs';
9
10
 
10
11
  const sha = bytes => `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
11
12
  function parse(bytes) {
@@ -52,7 +53,7 @@ export function lifecycleStatus({ root, checkpointRef, taskPackageRef }) {
52
53
  if (value.next_work_unit && !workUnit) issue('unknown-work-unit', `未知下一工作单元: ${value.next_work_unit}`, registryRef, '按注册表核对下一工作单元');
53
54
  if (workUnit && workUnit.id !== 'work-unit.entry-triage' && !contract.value.work_unit_routes?.[workUnit.id]) issue('missing-route', `缺少执行路由: ${workUnit.id}`, contractRef, '修复当前合同路由后重验');
54
55
  check('stage-and-route', diagnostics.some(x => ['unknown-stage', 'unknown-work-unit', 'missing-route'].includes(x.code)) ? 'failed' : 'passed', [registryRef, contractRef], '仅检查身份与路由存在,不计算完整 frontier');
55
- if (value.context_reconciliation?.status === 'blocked') issue('context-blocked', 'Context reconciliation blocked', checkpointRef, '修复词汇对账并执行适用校验');
56
+ if (value.context_reconciliation?.status === 'blocked') issue('context-blocked', '术语对账受阻', checkpointRef, '修复词汇对账并执行适用校验');
56
57
  check('context-reconciliation', value.context_reconciliation?.status === 'blocked' ? 'failed' : 'not-checked', [value.context_reconciliation?.ref].filter(Boolean), '读取登记状态;当前对账内容和摘要仍须由原验证器核验');
57
58
  for (const [id, gate] of Object.entries(value.gates ?? {})) {
58
59
  if (['blocked', 'stale'].includes(gate?.status)) issue('gate-blocked', `${id}: ${gate.status}`, checkpointRef, '核对受影响依据和当前批准');
@@ -142,7 +143,21 @@ export function lifecycleStatus({ root, checkpointRef, taskPackageRef }) {
142
143
  }
143
144
  const implementationArtifact=value.artifacts?.['artifact.vertical-slice-ticket'];
144
145
  const implementationCoverage=setRef && implementationArtifact?.ref?.endsWith('.md')?summarizeBusinessImplementation({root,setRef,sliceRefs:[implementationArtifact.ref]}):null;
146
+ const presenter = loadLifecyclePresenter(root);
147
+ const statusNames = { routing: '正在确定下一步', running: '正在推进', blocked: '受阻', 'paused-human-gate': '等待会签', completed: '记录为已完成,仍需核验' };
148
+ const readable = text => presenter.text(text, {trace: false}).replace(/: (blocked|stale)$/, (_, status) => status === 'blocked' ? ':受阻' : ':依据已过期,需重新核验');
149
+ const presentation = {
150
+ read_only: true, execution_allowed: false, approval_validity: 'not-checked',
151
+ stage: value.stage ? presenter.label(value.stage, {trace: false}) : '未登记',
152
+ checkpoint_status: Object.hasOwn(statusNames, value.status) ? statusNames[value.status] : `未识别状态(${value.status ?? '未登记'})`,
153
+ work_unit: value.next_work_unit ? presenter.label(value.next_work_unit, {trace: false}) : '未登记',
154
+ owner: presenter.text(owner, {trace: false}), blockers: blockers.map(readable),
155
+ expected_output: workUnit ? readable(workUnit.public_output ?? workUnit.output ?? '产出说明未登记') : null,
156
+ next_action: readable(next_action), sources: presenter.sources, warnings: presenter.warnings,
157
+ names: presenter.catalog({stage: value.stage, work_unit: value.next_work_unit, owner, gates: value.gates, artifacts: value.artifacts}),
158
+ };
145
159
  return {
160
+ presentation,
146
161
  decomposition: {business, implementation: {status:implementationArtifact?'recorded':'not-recorded',recorded_status:implementationArtifact?.status??null,ref:implementationArtifact?.ref??null,coverage:implementationCoverage}, readiness:{status:'not-evaluated',reason:'阶段工作完成不替代批准与实现就绪校验'}},
147
162
  schema_version: 1, read_only: true, stage: value.stage, checkpoint_status: value.status,
148
163
  work_unit: value.next_work_unit ?? null, owner, blockers, next_action,