@namewta/speculo 1.0.21 → 1.0.24

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 (152) hide show
  1. package/README.md +7 -5
  2. package/dist/src/media-state.d.ts +2 -0
  3. package/dist/src/media-state.js +52 -0
  4. package/dist/src/media-state.js.map +1 -0
  5. package/dist/src/structured.js +2 -0
  6. package/dist/src/structured.js.map +1 -1
  7. package/package.json +5 -4
  8. package/template/canonical/canonical-learning-lesson.md +116 -0
  9. package/template/canonical/canonical-learning-question.md +185 -0
  10. package/template/canonical/canonical-media-content.md +50 -0
  11. package/template/canonical/canonical-media-operations.md +53 -0
  12. package/template/canonical/canonical-media-video.md +48 -0
  13. package/template/canonical/canonical-media-visual.md +94 -0
  14. package/template/canonical/canonical-specdev-goal-plan.md +287 -0
  15. package/template/canonical/canonical-specdev-grill-with-docs.md +229 -0
  16. package/template/canonical/canonical-specdev-spec.md +277 -0
  17. package/template/canonical/canonical-specdev-tickets.md +287 -0
  18. package/template/canonical/canonical-specdev-wayfinder.md +8 -0
  19. package/template/workflows/learning/G-goal/probe-template.md +4 -2
  20. package/template/workflows/learning/G-goal/references/coverage-bar.md +4 -0
  21. package/template/workflows/learning/INDEX.md +2 -2
  22. package/template/workflows/learning/L-lesson/L-lesson.md +5 -1
  23. package/template/workflows/learning/L-lesson/lesson-template.md +4 -0
  24. package/template/workflows/learning/Q-question/Q-question.md +25 -33
  25. package/template/workflows/learning/Q-question/exploration-template.md +28 -0
  26. package/template/workflows/learning/Q-question/inquiry-template.md +4 -0
  27. package/template/workflows/learning/Q-question/question-map-template.json +4 -0
  28. package/template/workflows/learning/Q-question/references/exploration-mode.md +35 -0
  29. package/template/workflows/learning/Q-question/references/tutor-mode.md +31 -0
  30. package/template/workflows/learning/Q-question/tools/validate-exploration.mjs +190 -0
  31. package/template/workflows/learning/README.md +10 -4
  32. package/template/workflows/learning/common/rules/artifact-layouts.md +5 -0
  33. package/template/workflows/learning/common/rules/inquiry-depth-policy.md +31 -0
  34. package/template/workflows/learning/common/rules/lesson-contract.md +9 -1
  35. package/template/workflows/learning/common/rules/portable-learning-runtime.md +95 -0
  36. package/template/workflows/learning/common/rules/production-inquiry.md +24 -0
  37. package/template/workflows/learning/common/rules/question-map-contract.md +28 -0
  38. package/template/workflows/learning/common/rules/questioning-policy.md +12 -2
  39. package/template/workflows/learning/common/rules/teaching-policy.md +4 -0
  40. package/template/workflows/learning/common/skills/socratic-questioning/SKILL.md +2 -2
  41. package/template/workflows/media/B-brief/B-brief.md +18 -0
  42. package/template/workflows/media/B-brief/brief-template.json +32 -0
  43. package/template/workflows/media/D-deconstruct/D-deconstruct.md +18 -0
  44. package/template/workflows/media/H-hand-drawn/H-hand-drawn.md +18 -0
  45. package/template/workflows/media/H-hand-drawn/production.md +9 -0
  46. package/template/workflows/media/H-hand-drawn/style-catalog.json +226 -0
  47. package/template/workflows/media/I-init-setup/I-init-setup.md +18 -0
  48. package/template/workflows/media/INDEX.md +20 -0
  49. package/template/workflows/media/P-publish-draft/P-publish-draft.md +18 -0
  50. package/template/workflows/media/P-publish-draft/draft-request-template.json +14 -0
  51. package/template/workflows/media/R-retro/R-retro.md +18 -0
  52. package/template/workflows/media/R-retro/retro-template.md +13 -0
  53. package/template/workflows/media/README.md +76 -0
  54. package/template/workflows/media/V-video/V-video.md +18 -0
  55. package/template/workflows/media/V-video/production.md +9 -0
  56. package/template/workflows/media/V-video/render_preview.py +168 -0
  57. package/template/workflows/media/V-video/review-template.json +27 -0
  58. package/template/workflows/media/V-video/shot-plan-template.json +18 -0
  59. package/template/workflows/media/_state/archive/.gitkeep +1 -0
  60. package/template/workflows/media/_state/changes/.gitkeep +1 -0
  61. package/template/workflows/media/_state/context/INDEX.md +6 -0
  62. package/template/workflows/media/_state/status.json +6 -0
  63. package/template/workflows/media/common/rules/activation-and-memory.md +7 -0
  64. package/template/workflows/media/common/rules/artifact-contract.md +17 -0
  65. package/template/workflows/media/common/rules/content-method.md +18 -0
  66. package/template/workflows/media/common/rules/external-skills.md +33 -0
  67. package/template/workflows/media/common/rules/operations-method.md +21 -0
  68. package/template/workflows/media/common/rules/safety-and-recovery.md +15 -0
  69. package/template/workflows/media/common/rules/video-method.md +16 -0
  70. package/template/workflows/media/common/rules/visual-method.md +16 -0
  71. package/template/workflows/media/common/schemas/change-status.schema.json +75 -0
  72. package/template/workflows/media/common/schemas/status.schema.json +35 -0
  73. package/template/workflows/media/common/tests/media-tools.test.mjs +91 -0
  74. package/template/workflows/media/common/tools/media-tools.mjs +235 -0
  75. package/template/workflows/media/common/tools/validate-media.mjs +56 -0
  76. package/template/workflows/media/manifest.json +107 -0
  77. package/template/workflows/media/runtime-contract.json +10 -0
  78. package/template/workflows/specdev/A-archive-and-consolidate/A-archive-and-consolidate.md +6 -0
  79. package/template/workflows/specdev/A-archive-and-consolidate/consolidation-interview.md +4 -0
  80. package/template/workflows/specdev/C-code-review/C-code-review.md +7 -5
  81. package/template/workflows/specdev/C-code-review/code-review-template.md +8 -1
  82. package/template/workflows/specdev/D-diagnose-bugs/D-diagnose-bugs.md +10 -4
  83. package/template/workflows/specdev/D-diagnose-bugs/diagnosis-template.md +4 -0
  84. package/template/workflows/specdev/D-diagnose-bugs/feedback-loop.md +3 -1
  85. package/template/workflows/specdev/D-diagnose-bugs/hypothesis-and-instrumentation.md +4 -0
  86. package/template/workflows/specdev/D-diagnose-bugs/references/incident-evidence.md +21 -0
  87. package/template/workflows/specdev/G-grill-with-docs/G-grill-with-docs.md +6 -0
  88. package/template/workflows/specdev/G-grill-with-docs/grilling-protocol.md +4 -0
  89. package/template/workflows/specdev/G-grill-with-docs/references/interview-procedure.md +4 -0
  90. package/template/workflows/specdev/I-implement/I-implement.md +8 -2
  91. package/template/workflows/specdev/I-implement/evidence-template.md +4 -0
  92. package/template/workflows/specdev/I-implement/execution-preflight.md +4 -0
  93. package/template/workflows/specdev/I-implement/references/documentation-procedure.md +21 -0
  94. package/template/workflows/specdev/I-implement/references/implementation-procedure.md +7 -1
  95. package/template/workflows/specdev/I-implement/tdd-mocking.md +4 -0
  96. package/template/workflows/specdev/I-implement/tdd-rules.md +4 -0
  97. package/template/workflows/specdev/I-implement/tdd-test-design.md +4 -0
  98. package/template/workflows/specdev/I-init-setup/I-init-setup.md +4 -0
  99. package/template/workflows/specdev/INDEX.md +4 -0
  100. package/template/workflows/specdev/L-learn-change/L-learn-change.md +4 -0
  101. package/template/workflows/specdev/L-learn-change/references/lesson-format.md +4 -0
  102. package/template/workflows/specdev/P-goal-plan/P-goal-plan.md +6 -0
  103. package/template/workflows/specdev/P-goal-plan/completion-control.md +4 -0
  104. package/template/workflows/specdev/P-goal-plan/lead-orchestration.md +4 -0
  105. package/template/workflows/specdev/P-goal-plan/planning-modes.md +4 -0
  106. package/template/workflows/specdev/P-prototype/P-prototype.md +4 -0
  107. package/template/workflows/specdev/P-prototype/generate-design-package.md +4 -0
  108. package/template/workflows/specdev/P-prototype/style-selection-protocol.md +4 -0
  109. package/template/workflows/specdev/R-retro/R-retro.md +4 -0
  110. package/template/workflows/specdev/R-retro/retro-template.md +4 -0
  111. package/template/workflows/specdev/R-review-architecture/R-review-architecture.md +14 -4
  112. package/template/workflows/specdev/R-review-architecture/architecture-review-template.md +4 -0
  113. package/template/workflows/specdev/R-review-architecture/references/performance-review.md +27 -0
  114. package/template/workflows/specdev/R-review-architecture/references/selected-design.md +4 -0
  115. package/template/workflows/specdev/R-review-architecture/review-rubric.md +4 -0
  116. package/template/workflows/specdev/README.md +14 -6
  117. package/template/workflows/specdev/S-spec/S-spec.md +4 -0
  118. package/template/workflows/specdev/S-spec/spec-template.md +4 -0
  119. package/template/workflows/specdev/T-tickets/T-tickets.md +6 -0
  120. package/template/workflows/specdev/T-tickets/decomposition-rules.md +4 -0
  121. package/template/workflows/specdev/T-tickets/ticket-readiness.md +4 -0
  122. package/template/workflows/specdev/T-tickets/ticket-template.md +4 -0
  123. package/template/workflows/specdev/T-triage/T-triage.md +7 -1
  124. package/template/workflows/specdev/T-triage/ci-security-protocol.md +4 -0
  125. package/template/workflows/specdev/T-triage/pr-delivery-protocol.md +4 -0
  126. package/template/workflows/specdev/T-triage/release-preflight.md +4 -0
  127. package/template/workflows/specdev/T-triage/release-protocol.md +4 -0
  128. package/template/workflows/specdev/T-triage/release-recovery.md +4 -0
  129. package/template/workflows/specdev/W-wayfinder/W-wayfinder.md +4 -0
  130. package/template/workflows/specdev/W-wayfinder/investigation-ticket-template.md +4 -0
  131. package/template/workflows/specdev/common/README.md +8 -0
  132. package/template/workflows/specdev/common/rules/agent-experience.md +23 -0
  133. package/template/workflows/specdev/common/rules/artifact-contract.md +6 -0
  134. package/template/workflows/specdev/common/rules/codebase-design.md +16 -14
  135. package/template/workflows/specdev/common/rules/documentation-standard.md +29 -0
  136. package/template/workflows/specdev/common/rules/engineering-evidence.md +31 -0
  137. package/template/workflows/specdev/common/rules/error-handling.md +36 -0
  138. package/template/workflows/specdev/common/rules/evidence-and-verification.md +16 -0
  139. package/template/workflows/specdev/common/rules/test-reliability.md +33 -0
  140. package/template/workflows/specdev/common/rules/workflow-routing.md +13 -0
  141. package/template/workflows/specdev/common/rules/workflow-state-and-lifecycle.md +6 -0
  142. package/template/workflows/specdev/common/skills/code-review/SKILL.md +4 -2
  143. package/template/workflows/specdev/common/skills/code-review/references/result-reuse.md +1 -1
  144. package/template/workflows/specdev/common/tools/README.md +12 -2
  145. package/template/workflows/specdev/common/tools/engineering-evidence.mjs +150 -0
  146. package/template/workflows/specdev/common/tools/review-scope.mjs +82 -0
  147. package/template/workflows/specdev/common/tools/tests/engineering.test.mjs +267 -0
  148. package/template/workflows/specdev/common/tools/validate-specdev.mjs +27 -5
  149. package/template/skills/optimize-codex-config/SKILL.md +0 -81
  150. package/template/skills/optimize-codex-config/references/configuration-contract.md +0 -103
  151. package/template/skills/optimize-codex-config/references/troubleshooting.md +0 -79
  152. package/template/skills/optimize-codex-config/scripts/audit-codex-config.mjs +0 -747
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env node
2
+ /** Local-only primitives. No publishing API, installer or network client. */
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ import { existsSync, lstatSync, readFileSync, readdirSync, mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
5
+ import { dirname, isAbsolute, join, parse, resolve } from 'node:path';
6
+ import { spawnSync } from 'node:child_process';
7
+ import { fileURLToPath, pathToFileURL } from 'node:url';
8
+ const workflow = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
9
+ const ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
10
+ const CHANGE = /^\d{4}-\d{2}-\d{2}-[a-z0-9]+(?:-[a-z0-9]+)*$/;
11
+ export const WORKS = ['init-setup', 'brief', 'deconstruct', 'video', 'hand-drawn', 'publish-draft', 'retro'].map(x => `media/${x}`);
12
+ const text = x => typeof x === 'string' && x.trim().length > 0;
13
+ const object = x => x !== null && typeof x === 'object' && !Array.isArray(x);
14
+ const unique = x => Array.isArray(x) && x.every(text) && new Set(x).size === x.length;
15
+ function need(ok, code) { if (!ok) throw new Error(`media:${code}`); }
16
+ function keys(x, names) { need(object(x) && names.every(k => k in x) && Object.keys(x).every(k => names.includes(k)), 'unknown-or-missing-fields'); }
17
+ export function canonical(x) {
18
+ if (Array.isArray(x)) return '[' + x.map(canonical).join(',') + ']';
19
+ if (object(x)) return '{' + Object.keys(x).sort().map(k => JSON.stringify(k) + ':' + canonical(x[k])).join(',') + '}';
20
+ need(x === null || ['string', 'boolean', 'number'].includes(typeof x), 'not-json');
21
+ need(typeof x !== 'number' || Number.isFinite(x), 'not-finite');
22
+ return JSON.stringify(x);
23
+ }
24
+ export const digest = x => createHash('sha256').update(Buffer.isBuffer(x) ? x : canonical(x)).digest('hex');
25
+ export const readJSON = file => JSON.parse(readFileSync(file, 'utf8'));
26
+ /** Cooperative drift guard, not an atomic sandbox against hostile processes. */
27
+ export function localPath(root, relative, mustExist = true) {
28
+ need(text(relative) && !isAbsolute(relative) && !/[\\:\0]/.test(relative), 'unsafe-path');
29
+ const parts = relative.split('/'); need(parts.every(p => p && p !== '.' && p !== '..'), 'unsafe-path');
30
+ let current = resolve(root); need(existsSync(current) && lstatSync(current).isDirectory(), 'missing-root');
31
+ // Reject linked ancestors as well as the final root and child components.
32
+ for (let p = current; ; p = dirname(p)) {
33
+ need(!lstatSync(p).isSymbolicLink(), 'symlink');
34
+ if (p === parse(p).root) break;
35
+ }
36
+ for (const p of parts) {
37
+ current = join(current, p);
38
+ try { need(!lstatSync(current).isSymbolicLink(), 'symlink'); }
39
+ catch (e) { if (e.code !== 'ENOENT') throw e; if (mustExist) throw new Error('media:missing-file'); }
40
+ }
41
+ return current;
42
+ }
43
+ export function validateStatus(s, changes = {}) {
44
+ keys(s, ['schema_version', 'workflow', 'active', 'completed']);
45
+ need(s.schema_version === 1 && s.workflow === 'media', 'state-version');
46
+ for (const k of ['active', 'completed']) need(unique(s[k]) && s[k].every(n => CHANGE.test(n)), 'state-index');
47
+ need(new Set([...s.active, ...s.completed]).size === s.active.length + s.completed.length, 'overlapping-index');
48
+ for (const name of [...s.active, ...s.completed]) {
49
+ const c = changes[name]; keys(c, ['schema_version', 'change', 'status', 'current_work', 'works_run', 'revision', 'blockers', 'updated_at']);
50
+ need(c.schema_version === 1 && c.change === name, 'change-identity');
51
+ need(['active', 'blocked', 'completed', 'cancelled'].includes(c.status), 'change-status');
52
+ need(c.current_work === null || WORKS.includes(c.current_work), 'current-work');
53
+ need(unique(c.works_run) && c.works_run.every(x => WORKS.includes(x)) && unique(c.blockers), 'state-lists');
54
+ need(Number.isInteger(c.revision) && c.revision > 0, 'revision');
55
+ need(text(c.updated_at) && /^\d{4}-\d{2}-\d{2}T.*Z$/.test(c.updated_at) && Number.isFinite(Date.parse(c.updated_at)), 'state-time');
56
+ const terminal = ['completed', 'cancelled'].includes(c.status);
57
+ need(s.completed.includes(name) === terminal && (!terminal || c.current_work === null), 'terminal-index');
58
+ need(c.status !== 'completed' || !c.blockers.length, 'completion-blocked');
59
+ need(c.status !== 'blocked' || c.blockers.length > 0, 'missing-blocker');
60
+ }
61
+ return s;
62
+ }
63
+ export function validateStateRoot(root) {
64
+ const s = readJSON(localPath(root, 'status.json'));
65
+ need(Array.isArray(s.active) && Array.isArray(s.completed), 'state-index');
66
+ const changes = {};
67
+ for (const name of [...s.active, ...s.completed]) {
68
+ need(typeof name === 'string' && CHANGE.test(name), 'change-name');
69
+ changes[name] = readJSON(localPath(root, `changes/${name}/.status.json`));
70
+ }
71
+ return validateStatus(s, changes);
72
+ }
73
+ export function validateEvidence(claims, sources, final = false) {
74
+ need(Array.isArray(claims) && Array.isArray(sources), 'evidence-arrays');
75
+ need(sources.every(s => object(s) && ['id', 'locator', 'evidence'].every(k => text(s[k]))), 'source-incomplete');
76
+ const ids = sources.map(s => s.id); need(new Set(ids).size === ids.length, 'duplicate-source');
77
+ const claimIds = [];
78
+ for (const c of claims) {
79
+ need(object(c) && text(c.id) && text(c.text) && ['fact', 'opinion', 'hypothesis'].includes(c.kind), 'claim-incomplete');
80
+ claimIds.push(c.id);
81
+ need(['verified', 'pending', 'not-applicable'].includes(c.evidence_status) && unique(c.source_ids), 'claim-evidence');
82
+ need(c.source_ids.every(id => ids.includes(id)), 'missing-source');
83
+ if (c.kind === 'fact') {
84
+ need(c.evidence_status !== 'not-applicable', 'fact-needs-evidence');
85
+ if (c.evidence_status === 'verified') need(c.source_ids.length > 0, 'verified-without-source');
86
+ if (final) need(c.evidence_status === 'verified', 'pending-fact');
87
+ }
88
+ }
89
+ need(new Set(claimIds).size === claimIds.length, 'duplicate-claim');
90
+ }
91
+ export function validateBrief(b, final = false) {
92
+ need(object(b) && b.schema_version === 1, 'brief-version');
93
+ need(['content_id', 'topic', 'conclusion', 'audience', 'platform', 'style', 'font'].every(k => text(b[k])), 'missing-input');
94
+ need(ID.test(b.content_id), 'content-id');
95
+ need(Number.isFinite(b.duration_seconds) && b.duration_seconds > 0 && b.duration_seconds <= 600, 'duration');
96
+ for (const k of ['width', 'height']) need(Number.isInteger(b[k]) && b[k] >= 128 && b[k] <= 3840 && b[k] % 2 === 0, 'dimensions');
97
+ need(Number.isInteger(b.fps) && b.fps >= 1 && b.fps <= 60, 'fps');
98
+ need(Array.isArray(b.colors) && b.colors.length > 0 && b.colors.every(text), 'colors');
99
+ need(object(b.voice) && ['silent', 'provided'].includes(b.voice.mode), 'voice');
100
+ const kinds = ['result', 'pain', 'counterintuitive'];
101
+ need(Array.isArray(b.hooks) && b.hooks.every(h => object(h) && text(h.text) && kinds.includes(h.kind)) && kinds.every(k => b.hooks.some(h => h.kind === k)), 'three-hooks');
102
+ validateEvidence(b.claims, b.sources, final); return b;
103
+ }
104
+ export function validateVideo(b, p, root) {
105
+ validateBrief(b, true); need(object(p) && p.schema_version === 1 && text(p.narration), 'narration-first');
106
+ need(Array.isArray(p.shots) && p.shots.length > 0 && p.shots.length <= 100, 'shots');
107
+ if (b.voice.mode === 'silent') need(p.audio === null && p.transcript === null, 'silent-no-fake-transcript');
108
+ else {
109
+ need(root && text(p.audio) && lstatSync(localPath(root, p.audio)).isFile(), 'audio-first');
110
+ need(Array.isArray(p.transcript) && p.transcript.length > 0, 'transcript-first');
111
+ let end = 0;
112
+ for (const line of p.transcript) {
113
+ need(object(line) && Number.isFinite(line.start) && Number.isFinite(line.end) && line.start >= end && line.end > line.start && line.end <= b.duration_seconds && text(line.text), 'transcript-timing'); end = line.end;
114
+ }
115
+ }
116
+ let end = 0; const ids = [];
117
+ for (const s of p.shots) {
118
+ need(object(s) && text(s.id) && ID.test(s.id), 'shot-id'); ids.push(s.id);
119
+ need(Number.isFinite(s.start) && Number.isFinite(s.end) && Math.abs(s.start - end) < 1e-6 && s.end > s.start && s.end <= b.duration_seconds + 1e-6, 'shot-timeline');
120
+ need(['message', 'narration', 'caption', 'visual'].every(k => text(s[k])) && unique(s.assets), 'shot-content');
121
+ for (const path of s.assets) need(root && lstatSync(localPath(root, path)).isFile(), 'missing-asset'); end = s.end;
122
+ }
123
+ need(new Set(ids).size === ids.length && Math.abs(end - b.duration_seconds) < 1e-6, 'shot-coverage'); return p;
124
+ }
125
+ export function validateReview(r, b, p, inputDigest) {
126
+ need(object(r) && r.evidence_kind === 'executed' && text(r.reviewer), 'review-not-observed');
127
+ need(r.input_digest === inputDigest, 'stale-review');
128
+ const asset = x => object(x) && text(x.path) && /^[a-f0-9]{64}$/.test(x.sha256 ?? '');
129
+ need(asset(r.preview) && Number.isFinite(r.preview.duration_seconds), 'preview-evidence');
130
+ const d = r.preview.duration_seconds;
131
+ need(b.duration_seconds < 15 ? Math.abs(d - b.duration_seconds) <= 1 / b.fps : d >= 15 && d <= Math.min(30, b.duration_seconds) + 1 / b.fps, 'preview-duration');
132
+ need(object(r.checks) && ['fonts', 'captions', 'aspect', 'rhythm', 'audio_sync', 'determinism'].every(k => r.checks[k] === true), 'review-checks');
133
+ need(object(r.scores) && ['accuracy', 'readability', 'visual_consistency', 'rhythm', 'audio_visual'].every(k => Number.isFinite(r.scores[k]) && r.scores[k] >= 8 && r.scores[k] <= 10), 'review-score');
134
+ need(Array.isArray(r.blockers) && r.blockers.length === 0, 'review-blockers');
135
+ need(Array.isArray(r.stills) && r.stills.length === p.shots.length * 3 && p.shots.every(s => r.stills.filter(f => asset(f) && f.shot_id === s.id && f.accepted === true).length === 3), 'three-stills');
136
+ need(new Set(r.stills.map(s => s.path)).size === r.stills.length, 'duplicate-still'); return r;
137
+ }
138
+ export const styleMenu = () => readJSON(join(workflow, 'H-hand-drawn/style-catalog.json')).styles;
139
+ export function selectStyle(value) {
140
+ need(text(value), 'select-style'); const key = value.trim().toLowerCase();
141
+ const s = styleMenu().find(s => [s.id, s.name, ...s.aliases].some(x => x.toLowerCase() === key)); need(s, 'unknown-style'); return s;
142
+ }
143
+ export function fingerprintPackage(root) {
144
+ localPath(root, 'STYLES.md'); const rows = []; let total = 0;
145
+ function walk(dir, prefix = '') {
146
+ for (const name of readdirSync(dir).sort()) {
147
+ if (['.git', '__pycache__'].includes(name)) continue;
148
+ const file = join(dir, name), stat = lstatSync(file), relative = prefix + name;
149
+ need(!stat.isSymbolicLink(), 'skill-symlink');
150
+ if (stat.isDirectory()) walk(file, relative + '/');
151
+ else { total += stat.size; need(stat.isFile() && stat.size <= 4 * 1024 * 1024 && total <= 32 * 1024 * 1024, 'skill-size'); rows.push([relative, digest(readFileSync(file))]); }
152
+ }
153
+ }
154
+ walk(root); return digest(rows);
155
+ }
156
+ export function renderPrompt({ skillRoot, reviewedDigest, style, variables = {}, subject, title, text: literal, aspect, python = 'python3' }) {
157
+ const selected = selectStyle(style);
158
+ need(/^[a-f0-9]{64}$/.test(reviewedDigest ?? '') && fingerprintPackage(skillRoot) === reviewedDigest, 'skill-review-required');
159
+ for (const f of ['LICENSE', 'STYLES.md', 'PROTOCOL.md', 'PACKAGE-MANIFEST.json', 'scripts/render_prompt.py', 'scripts/hosted_images.py', 'assets/image-manifest.json']) localPath(skillRoot, f);
160
+ if (selected.id === '20') localPath(skillRoot, 'scripts/validate_style_20_asset.py');
161
+ const version = spawnSync(python, ['-I', '-B', '-c', 'import sys; print("%d.%d" % sys.version_info[:2])'], { encoding: 'utf8', timeout: 5000, shell: false });
162
+ const match = version.stdout?.trim().match(/^(\d+)\.(\d+)$/);
163
+ need(version.status === 0 && match && (Number(match[1]) > 3 || Number(match[1]) === 3 && Number(match[2]) >= 10), 'python-3.10-required');
164
+ const args = ['-B', localPath(skillRoot, 'scripts/render_prompt.py'), '--style', selected.id];
165
+ for (const [flag, value] of [['--subject', subject], ['--title', title], ['--text', literal], ['--aspect', aspect]]) if (value !== undefined) { need(text(value), 'empty-prompt-argument'); args.push(`${flag}=${value}`); }
166
+ need(object(variables), 'prompt-variables');
167
+ for (const [k, v] of Object.entries(variables)) { need(/^[\p{L}_][\p{L}\p{N}_]*$/u.test(k) && typeof v === 'string', 'prompt-variable'); args.push('--var', `${k}=${v}`); }
168
+ if (selected.requires_references) args.push('--format', 'json');
169
+ const env = Object.fromEntries(['PATH', 'HOME', 'USERPROFILE', 'SYSTEMROOT', 'TEMP', 'TMP', 'HAND_DRAWN_IMAGE_CACHE'].filter(k => process.env[k]).map(k => [k, process.env[k]]));
170
+ Object.assign(env, { HAND_DRAWN_OFFLINE: '1', PYTHONDONTWRITEBYTECODE: '1', PYTHONNOUSERSITE: '1', PYTHONUTF8: '1' });
171
+ const out = spawnSync(python, args, { cwd: resolve(skillRoot), env, shell: false, timeout: 20000, maxBuffer: 2 * 1024 * 1024 });
172
+ need(out.status === 0 && !out.error && out.stdout?.length > 0, 'prompt-failed-offline');
173
+ need(fingerprintPackage(skillRoot) === reviewedDigest, 'skill-changed'); return out.stdout;
174
+ }
175
+ export function detectDependencies() {
176
+ const commands = { node: [process.execPath, ['--version']], python: ['python3', ['-I', '-c', 'import sys; print(sys.version.split()[0]); sys.exit(0 if sys.version_info >= (3,10) else 1)']], ffmpeg: ['ffmpeg', ['-version']], ffprobe: ['ffprobe', ['-version']], uv: ['uv', ['--version']], chromium: ['chromium', ['--version']], playwright: ['python3', ['-I', '-c', 'import importlib.util; print("present" if importlib.util.find_spec("playwright") else "missing")']] };
177
+ return Object.fromEntries(Object.entries(commands).map(([k, [exe, args]]) => { const r = spawnSync(exe, args, { encoding: 'utf8', timeout: 5000, maxBuffer: 16384, shell: false }); const detail = (r.stdout || r.stderr || '').trim().split('\n')[0]; return [k, { available: r.status === 0 && detail !== 'missing', detail: detail || r.error?.code || 'unavailable' }]; }));
178
+ }
179
+ export function draftFingerprint(r, sourceRoot) {
180
+ need(object(r) && unique(r.media) && r.media.length <= 50, 'draft-media');
181
+ const { approvals, ...payload } = r;
182
+ const files = r.media.map(f => { need(sourceRoot, 'media-root'); const path = localPath(sourceRoot, f); need(lstatSync(path).isFile() && lstatSync(path).size <= 128 * 1024 * 1024, 'media-file'); return [f, digest(readFileSync(path))]; });
183
+ return digest({ payload, files });
184
+ }
185
+ export function validateDraft(r, sourceRoot) {
186
+ need(object(r) && r.schema_version === 1 && ID.test(r.content_id ?? '') && Number.isInteger(r.revision) && r.revision > 0, 'draft-identity');
187
+ need(['channel', 'account', 'title', 'body'].every(k => text(r[k])), 'draft-inputs');
188
+ need(r.action === undefined || r.action === 'local-draft', 'external-action-disabled');
189
+ need(['not-authorized', 'external-human-handoff'].includes(r.publication_authorization), 'publication-disabled');
190
+ validateEvidence(r.claims, r.sources, true); const fp = draftFingerprint(r, sourceRoot); need(Array.isArray(r.approvals), 'approvals');
191
+ for (const gate of ['direction', 'platform', 'title', 'final']) {
192
+ const a = r.approvals.filter(a => object(a) && a.gate === gate);
193
+ need(a.length === 1 && a[0].approved === true && text(a[0].by) && text(a[0].at) && Number.isFinite(Date.parse(a[0].at)) && a[0].digest === fp, `approval-${gate}`);
194
+ }
195
+ return fp;
196
+ }
197
+ export function exportDraft(r, outputRoot, sourceRoot) {
198
+ const fp = validateDraft(r, sourceRoot), name = `${r.content_id}-r${r.revision}-${fp.slice(0, 12)}`;
199
+ const target = localPath(outputRoot, name, false);
200
+ const media = r.media.map((f, i) => ({ source: f, path: `media/${i + 1}-${f.split('/').at(-1)}`, sha256: digest(readFileSync(localPath(sourceRoot, f))) }));
201
+ const md = `# ${r.title}\n\n${r.body}\n\n---\nLocal draft only. No upload, login or publication was performed.\n`;
202
+ const json = JSON.stringify({ ...r, media_manifest: media, input_digest: fp, status: 'local-draft-ready', publication_performed: false, side_effects: [] }, null, 2) + '\n';
203
+ if (existsSync(target)) {
204
+ need(readFileSync(localPath(target, 'draft.md'), 'utf8') === md && readFileSync(localPath(target, 'package.json'), 'utf8') === json, 'draft-drift');
205
+ for (const f of media) need(digest(readFileSync(localPath(target, f.path))) === f.sha256, 'draft-media-drift');
206
+ return { path: target, reused: true, digest: fp, side_effects: [] };
207
+ }
208
+ const lock = localPath(outputRoot, `${name}.lock`, false); mkdirSync(lock);
209
+ const stage = join(outputRoot, `${name}.tmp-${randomUUID()}`);
210
+ try {
211
+ mkdirSync(stage); writeFileSync(join(stage, 'draft.md'), md, { flag: 'wx' }); writeFileSync(join(stage, 'package.json'), json, { flag: 'wx' });
212
+ if (media.length) mkdirSync(join(stage, 'media'));
213
+ for (const f of media) writeFileSync(join(stage, f.path), readFileSync(localPath(sourceRoot, f.source)), { flag: 'wx' });
214
+ need(validateDraft(r, sourceRoot) === fp, 'source-drift');
215
+ for (const f of media) need(digest(readFileSync(join(stage, f.path))) === f.sha256, 'copy-drift');
216
+ need(!existsSync(target), 'concurrent-draft'); renameSync(stage, target);
217
+ return { path: target, reused: false, digest: fp, side_effects: [] };
218
+ } finally { if (existsSync(stage)) rmSync(stage, { recursive: true }); rmSync(lock, { recursive: true }); }
219
+ }
220
+ export function cli(args) {
221
+ const [command, ...a] = args;
222
+ if (command === 'detect' && !a.length) return detectDependencies();
223
+ if (command === 'style-menu' && !a.length) return styleMenu();
224
+ if (command === 'check-brief' && a.length === 1) { validateBrief(readJSON(a[0])); return { valid: true }; }
225
+ if (command === 'check-video' && a.length === 3) { validateVideo(readJSON(a[0]), readJSON(a[1]), a[2]); return { valid: true }; }
226
+ if (command === 'fingerprint-skill' && a.length === 1) return { digest: fingerprintPackage(a[0]), approved: false };
227
+ if (command === 'render-prompt' && a.length === 1) { process.stdout.write(renderPrompt(readJSON(a[0]))); return; }
228
+ if (command === 'fingerprint-draft' && a.length >= 1 && a.length <= 2) return { digest: draftFingerprint(readJSON(a[0]), a[1]), approved: false };
229
+ if (command === 'export-draft' && a.length >= 2 && a.length <= 3) return exportDraft(readJSON(a[0]), a[1], a[2]);
230
+ throw new Error('usage: media-tools.mjs detect | style-menu | check-brief FILE | check-video BRIEF PLAN SOURCE_ROOT | fingerprint-skill DIR | render-prompt REQUEST | fingerprint-draft REQUEST [SOURCE_ROOT] | export-draft REQUEST EXISTING_OUTPUT_ROOT [SOURCE_ROOT]');
231
+ }
232
+ if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) {
233
+ try { const result = cli(process.argv.slice(2)); if (result !== undefined) console.log(JSON.stringify(result, null, 2)); }
234
+ catch (e) { console.error(e.message); process.exitCode = 1; }
235
+ }
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ /** Read-only package/runtime validation; never initializes or repairs state. */
3
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
4
+ import { join, resolve, dirname } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { readJSON, validateStateRoot, validateStatus } from './media-tools.mjs';
7
+ let workflow = resolve(dirname(fileURLToPath(import.meta.url)), '../..'), state;
8
+ const args = process.argv.slice(2), errors = [];
9
+ const need = (ok, label) => { if (!ok) errors.push(label); };
10
+ try {
11
+ for (let i = 0; i < args.length; i++) {
12
+ if (!['--workflow-root', '--state-root'].includes(args[i]) || !args[i + 1] || args[i + 1].startsWith('--')) throw new Error('unknown/incomplete argument');
13
+ const flag = args[i++]; if (flag === '--workflow-root') workflow = resolve(args[i]); else state = resolve(args[i]);
14
+ }
15
+ const index = readFileSync(join(workflow, 'INDEX.md'), 'utf8'), readme = readFileSync(join(workflow, 'README.md'), 'utf8');
16
+ need(/^type: workflow\s*$/m.test(index) && /^workflow: media\s*$/m.test(index), 'INDEX identity');
17
+ need(index.includes('永久知识') && index.includes('Work 激活') && !index.includes('AUTO-INDEX-START'), 'passive INDEX');
18
+ need((readme.match(/AUTO-INDEX-START/g) || []).length === 1 && (readme.match(/AUTO-INDEX-END/g) || []).length === 1, 'README markers');
19
+ for (const title of ['Work 条目', '运行时根', '持久化约定', '启动协议', '状态字段', '路径分配', '副作用边界']) need(readme.includes(`## ${title}`), `README ${title}`);
20
+ const manifest = readJSON(join(workflow, 'manifest.json'));
21
+ need(manifest.schema_version === 1 && manifest.id === 'media' && /^\d+\.\d+\.\d+$/.test(manifest.version), 'manifest identity');
22
+ need(JSON.stringify(manifest.persistent_knowledge) === JSON.stringify(['<Path>{roots.state}/media/context/INDEX.md</Path>']), 'knowledge registration');
23
+ const dirs = readdirSync(workflow, { withFileTypes: true }).filter(e => e.isDirectory() && /^[A-Z]-/.test(e.name)).map(e => e.name);
24
+ need(dirs.length === 7 && manifest.stages?.length === 7, 'seven Works');
25
+ const ids = [];
26
+ for (const dir of dirs) {
27
+ const content = readFileSync(join(workflow, dir, `${dir}.md`), 'utf8');
28
+ ids.push(content.match(/^id: media\/(.+)$/m)?.[1].trim());
29
+ need(content.includes('<Path>{roots.workflows}/media/README.md</Path>') && content.includes('common/rules/activation-and-memory.md'), `${dir} activation`);
30
+ need(/^type: workflow-entry\s*$/m.test(content) && readme.includes(`**${dir}**`), `${dir} discovery`);
31
+ }
32
+ need(new Set(ids).size === 7 && manifest.stages.every(s => ids.includes(s.id) && ['read-only', 'local-reversible'].includes(s.risk) && Number.isInteger(s.context_budget) && s.context_budget > 0 && ['inputs', 'outputs', 'after'].every(k => Array.isArray(s[k])) && s.after.every(x => ids.includes(x))), 'stage contract');
33
+ const menu = readJSON(join(workflow, 'H-hand-drawn/style-catalog.json'));
34
+ need(menu.recipes_vendored === false && menu.styles.length === 21 && new Set(menu.styles.map(s => s.id)).size === 21, '21 styles metadata');
35
+ const runtime = readJSON(join(workflow, 'runtime-contract.json'));
36
+ need(runtime.schema_version === 1 && runtime.workflow === 'media' && runtime.opaque_default === 'preserve-byte-for-byte', 'runtime contract');
37
+ for (const file of readdirSync(join(workflow, 'common/schemas'))) readJSON(join(workflow, 'common/schemas', file));
38
+ // The installer excludes _state from static assets and seeds the runtime separately.
39
+ // Package mode still requires the seed; runtime mode validates the explicit state below.
40
+ if (!state) validateStatus(readJSON(join(workflow, '_state/status.json')));
41
+ const registry = readFileSync(join(workflow, 'common/rules/external-skills.md'), 'utf8');
42
+ for (const name of ['Punk-Skill', 'video-talkcraft', 'yichen-skills', 'jianying-headless', 'jianying-editor-skill', 'content-boom-monitor', 'video-to-subtitle-summary-skill', 'baoyu-skills', 'HyperFrames', 'everything-claude-code', 'hand-drawn-styles', 'video-use', 'huashu-art-motion']) need(registry.includes(name), `registry: ${name}`);
43
+ function walk(dir) {
44
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
45
+ const file = join(dir, e.name); if (e.isDirectory()) walk(file);
46
+ else if (e.name.endsWith('.md')) {
47
+ const text = readFileSync(file, 'utf8');
48
+ need(!/\{roots\.workflows\}\/media\/_state/.test(text), `runtime points to seed: ${file}`);
49
+ for (const match of text.matchAll(/<Path>\{roots\.workflows\}\/media\/([^<]*)<\/Path>/g)) if (!/[{}]/.test(match[1])) need(existsSync(join(workflow, match[1])), `missing reference: ${match[1]}`);
50
+ }
51
+ }
52
+ }
53
+ walk(workflow); if (state) validateStateRoot(state);
54
+ } catch (e) { errors.push(e.message); }
55
+ if (errors.length) { console.error(errors.join('\n')); process.exitCode = 1; }
56
+ else console.log(`media validation: ok (${state ? 'package and runtime' : 'package only; no runtime read/write'})`);
@@ -0,0 +1,107 @@
1
+ {
2
+ "schema_version": 1,
3
+ "id": "media",
4
+ "version": "1.0.0",
5
+ "persistent_knowledge": [
6
+ "<Path>{roots.state}/media/context/INDEX.md</Path>"
7
+ ],
8
+ "stages": [
9
+ {
10
+ "id": "init-setup",
11
+ "after": [],
12
+ "inputs": [
13
+ "user-request",
14
+ "current-owned-evidence"
15
+ ],
16
+ "outputs": [
17
+ "init-setup-artifacts"
18
+ ],
19
+ "risk": "local-reversible",
20
+ "context_budget": 8000
21
+ },
22
+ {
23
+ "id": "brief",
24
+ "after": [],
25
+ "inputs": [
26
+ "user-request",
27
+ "current-owned-evidence"
28
+ ],
29
+ "outputs": [
30
+ "brief-artifacts"
31
+ ],
32
+ "risk": "local-reversible",
33
+ "context_budget": 16000
34
+ },
35
+ {
36
+ "id": "deconstruct",
37
+ "after": [],
38
+ "inputs": [
39
+ "user-request",
40
+ "current-owned-evidence"
41
+ ],
42
+ "outputs": [
43
+ "deconstruct-artifacts"
44
+ ],
45
+ "risk": "local-reversible",
46
+ "context_budget": 8000
47
+ },
48
+ {
49
+ "id": "video",
50
+ "after": [
51
+ "brief"
52
+ ],
53
+ "inputs": [
54
+ "user-request",
55
+ "current-owned-evidence"
56
+ ],
57
+ "outputs": [
58
+ "video-artifacts"
59
+ ],
60
+ "risk": "local-reversible",
61
+ "context_budget": 16000
62
+ },
63
+ {
64
+ "id": "hand-drawn",
65
+ "after": [],
66
+ "inputs": [
67
+ "user-request",
68
+ "current-owned-evidence"
69
+ ],
70
+ "outputs": [
71
+ "hand-drawn-artifacts"
72
+ ],
73
+ "risk": "local-reversible",
74
+ "context_budget": 8000
75
+ },
76
+ {
77
+ "id": "publish-draft",
78
+ "after": [
79
+ "brief"
80
+ ],
81
+ "inputs": [
82
+ "user-request",
83
+ "current-owned-evidence"
84
+ ],
85
+ "outputs": [
86
+ "publish-draft-artifacts"
87
+ ],
88
+ "risk": "local-reversible",
89
+ "context_budget": 8000
90
+ },
91
+ {
92
+ "id": "retro",
93
+ "after": [
94
+ "publish-draft"
95
+ ],
96
+ "inputs": [
97
+ "user-request",
98
+ "current-owned-evidence"
99
+ ],
100
+ "outputs": [
101
+ "retro-artifacts"
102
+ ],
103
+ "risk": "local-reversible",
104
+ "context_budget": 8000
105
+ }
106
+ ]
107
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "schema_version": 1,
3
+ "workflow": "media",
4
+ "config": null,
5
+ "structured_state": [
6
+ ".speculo/media/status.json",
7
+ ".speculo/media/changes/**/.status.json"
8
+ ],
9
+ "opaque_default": "preserve-byte-for-byte"
10
+ }
@@ -35,6 +35,12 @@ A 是 SpecDev 的归档 wrapper:它拥有模式选择、SpecDev 完成门和
35
35
 
36
36
  读取 `<Path>{roots.workflows}/specdev/A-archive-and-consolidate/consolidation-interview.md</Path>`,由本 Work 创建知识 change、按代码证据访谈并验证后关闭,再回到 Archive 模式。LOG/CONTEXT/ADR 先在该 change 内形成;未验证结论留在 LOG,不直接提升永久知识。
37
37
 
38
+ ## 晋升前的语义筛选
39
+
40
+ 检查当前性、跨 change 决策价值、实际实现证据、适用范围、真实备选和后果;已接受但尚未实现的计划不能写成当前产品事实。按未来决策价值毕业,不按年龄、字数或数量配额。机械局部修改可以不晋升,但原 change 与必要历史仍保留。
41
+
42
+ 同主题先判断复用、局部取代或完整取代,合并前保留独有理由、负向保证、验证义务与恢复条件。未毕业解释留在原 change;不复制 DSH 的 Notes 树、冻结清单或双语三件套。干预范围仍由原全局归档网关的 dry-run/confirmed 和漂移检查决定。
43
+
38
44
  ## 副作用
39
45
 
40
46
  Dry-run 不修改文件。归档移动、知识 merge/rewrite/delete、Git 动作均在计划展示后单独确认。归档目录完成后只读;后续纠正通过新 change 和 supersedes 链完成。
@@ -61,3 +61,7 @@ change 内三份文档复用 grill 的既有格式,避免重复发明:
61
61
  3. 按 LOG → CONTEXT → ADR 顺序写入:CONTEXT 只保存项目规范术语;ADR 只有同时满足难逆转、令人意外和真实权衡时才创建。
62
62
  4. 用户结论必须由当前代码或实际行为验证;未验证内容留在 LOG,不提升。
63
63
  5. 按 change completion 规则由本 Work 关闭该非实现型 change,再进入 Archive 模式的 dry-run/确认流程。
64
+
65
+ ## 未来价值与取代关系
66
+
67
+ 逐条核对当前实现、跨 change 用途、真实备选/代价、负向保证和恢复义务;不编造设计理由,不把未落地计划晋升为事实。同主题合并保留独有理由,局部替代保留剩余有效部分。未晋升内容随原 change 保留,永久写入仍走既有 A 网关。
@@ -3,8 +3,8 @@ id: specdev/code-review
3
3
  type: workflow-entry
4
4
  workflow: specdev
5
5
  name: 代码审查
6
- description: 将 commit、branch、tag、merge-base 或 PR 解析为本地不可变固定点,执行隔离的标准轴与规范轴审查并持久化可恢复报告。
7
- keywords: [code-review, review, diff, fixed-point, PR, 标准, 规范]
6
+ description: 按用户指定的 PR、单 commit、区间或基线冻结真实比较范围,执行隔离的标准轴与规范轴审查;有效空差异诚实返回,不实施修复或发布。
7
+ keywords: [code-review, review, diff, fixed-point, PR, 标准, 规范, no-diff, commit, range]
8
8
  ---
9
9
 
10
10
  # 代码审查
@@ -23,8 +23,8 @@ C 是独立 review 入口,不实施修复。它拥有 `<Path>{roots.state}/spe
23
23
 
24
24
  ## 流程
25
25
 
26
- 1. **解析固定点**:使用 `git rev-parse` 把固定点和 HEAD 固定为 SHA;PR 先通过 `<Path>{roots.workflows}/specdev/T-triage/remote-operations.md</Path>` 的 `pr-read` 获得 base/head SHA,并确保对应对象可在本地解析。
27
- 2. **冻结输入**:记录 `git diff <fixed>...<head>` 和 `git log <fixed>..<head> --oneline`。引用无效或 diff 为空时失败,不创建报告。
26
+ 1. **解析固定点**:先选择 baseline(共同祖先到 head)、range(两个指定端点)、commit(指定提交与其父)或 pr(远程 PR diff)。PR 先通过 <Path>{roots.workflows}/specdev/T-triage/remote-operations.md</Path> 的 pr-read 获得完整 base/head SHA。调用 <Path>{roots.workflows}/specdev/common/tools/validate-specdev.mjs</Path> 的 `--review-scope --repo <project-root> --mode <mode>`;端点模式传 --base/--head,commit 传 --commit,多父提交明确 --parent。此只读工具冻结本地对象,不抓取、不写状态;根提交没有现有 CR 的 committed baseline 时明确报告限制,不编造父提交。
27
+ 2. **冻结输入**:保存工具返回的实际 diff argv、fixed_point/head、diff_sha256、路径与 commit log,不把单 commit 扩大到当前 HEAD。引用无效则失败;有效空 diff 返回 no-diff,不启动 reviewer、不造 CR、不声称 approved;正常结束本次 Work 并报告固定输入,change 完成仍按原终点规则判断。有差异才继续。
28
28
  3. **发现来源**:调用 `<Path>{roots.workflows}/specdev/common/skills/code-review/SKILL.md</Path>`;规范来源缺失时经确认跳过规范轴,标准轴继续。
29
29
  4. **隔离审查**:仅按公共 Skill 的完整输入复用门接受既有结果;需要执行的轴在平台支持时并行 reviewer,否则用两个独立完整输入包顺序执行。两轴不得读取对方 finding。
30
30
  5. **持久化**:选择下一个未占用 `CR-###`,使用 `<Path>{roots.workflows}/specdev/C-code-review/code-review-template.md</Path>` 写入 `<Path>{roots.state}/specdev/changes/{change}/reviews/CR-###.md</Path>`,原子重读。
@@ -32,9 +32,11 @@ C 是独立 review 入口,不实施修复。它拥有 `<Path>{roots.state}/spe
32
32
 
33
33
  ## 完成标准
34
34
 
35
- - fixed point/head 是本地不可变 SHA,三点 diff 非空;
35
+ - 有差异分支的 fixed point/head 是本地不可变 SHA,diff 语义与用户选择一致;no-diff 独立返回,不运行要求 CR 的 review stage;
36
36
  - 规范和标准来源发现顺序有记录;
37
37
  - 两轴隔离、固定顺序、未合并排名;
38
38
  - 每个 finding 有路径、风险、依据和满足条件;
39
39
  - 报告、状态、验证结果和下一 Work 路径一致;
40
40
  - C 未修改项目代码或远程状态。
41
+
42
+ 审查接口双方、生命周期、取消/重试、资源释放、真实执行入口与可绕过路径;项目文档和模型可见内容变化分别按 <Path>{roots.workflows}/specdev/common/rules/documentation-standard.md</Path> 与 <Path>{roots.workflows}/specdev/common/rules/agent-experience.md</Path> 核对。base/head、配置或规范输入变化后重新评估受影响结论,不沿用旧 head 的通过。
@@ -17,7 +17,10 @@ created_at: <ISO-8601>
17
17
 
18
18
  ## Fixed Input
19
19
 
20
- - **Diff:** `git diff <fixed_point>...<head>`
20
+ - **Comparison mode:** baseline / range / commit / pr
21
+ - **Diff:** 实际冻结的 argv;legacy 未声明模式时才采用 `git diff <fixed_point>...<head>`
22
+ - **Diff digest:** 实际 diff 字节的 SHA-256
23
+ - **Original base/head:** 原始不可变引用;fixed_point 为实际比较起点
21
24
  - **Commits:** `git log <fixed_point>..<head> --oneline`
22
25
  - **Scope:**
23
26
 
@@ -44,3 +47,7 @@ created_at: <ISO-8601>
44
47
  ## 审查证据复用(仅实际发生时填写)
45
48
 
46
49
  记录原 CR/Evidence locator、fixed/head/diff、规范/标准/指令与 Skill/reference 摘要、工具/环境相关证据、两轴隔离核对、复用或重跑的轴与原因。未发生时写“不适用”;本节不是通过声明,也不改变原始审查结果。
50
+
51
+ ## 比较语义与证据有效期
52
+
53
+ 记录 baseline/range/commit/pr、实际 diff argv/digest、原 base/head 与 fixed point、消费者和相关配置/规范版本。合法 no-diff 不创建本 CR;非法引用和根提交无 committed baseline 明确报告。head 或相关输入变更后,核对受影响审查与验证,不沿用旧通过。
@@ -3,8 +3,8 @@ id: specdev/diagnose-bugs
3
3
  type: workflow-entry
4
4
  workflow: specdev
5
5
  name: 诊断 Bug
6
- description: 先建立会在精确症状上变红的紧凑反馈回路,再通过最小化、排名假设和单变量探针确认根因,输出修复契约而不实施生产修复。
7
- keywords: [bug, 诊断, 红灯, tight-loop, 根因, 复现, 假设]
6
+ description: 调查故障并建立精确症状的红灯回路,以单变量实验确认根因;无法安全复现时仅收集事件证据,不将解释冒充根因,也不实施生产修复。
7
+ keywords: [bug, 诊断, 红灯, tight-loop, 根因, 复现, 假设, incident, 事件证据]
8
8
  ---
9
9
 
10
10
  # 诊断 Bug
@@ -32,11 +32,11 @@ D 拥有:
32
32
 
33
33
  ## 流程
34
34
 
35
- ### 1. 建立并执行红灯回路
35
+ ### 1. 建立并执行红灯反馈回路
36
36
 
37
37
  加载 `<Path>{roots.workflows}/specdev/D-diagnose-bugs/feedback-loop.md</Path>`。按其顺序尝试测试、HTTP/CLI、浏览器、追踪回放、夹具、模糊循环、bisect、差分和最终 HITL。保存命令、至少一次真实输出、精确症状断言、运行时间、确定性或复现率,以及 Agent 可运行性。
38
38
 
39
- 无法建立回路时立即使用诊断模板写 `status: blocked`、`feedback_loop_ready: false`、已尝试方式和所需输入;假设表保持为空,保留 `current_work` 后返回。不得继续阅读代码来构造根因理论。
39
+ 无法建立回路时使用诊断模板写 `status: blocked`、`feedback_loop_ready: false`、已尝试方式和所需输入;假设表保持为空,保留 `current_work`。不得继续阅读代码来构造根因理论并据此宣称已确认;可以进入下述事件证据分支收集观察与下一安全检查,但不能直接进入正式假设实验或 Ready 修复。
40
40
 
41
41
  **完成标准**:一条已运行命令快速、可重复、由 Agent 执行,并能在用户精确症状上变红。
42
42
 
@@ -70,6 +70,12 @@ node <Path>{roots.workflows}/specdev/common/tools/validate-specdev.mjs</Path> \
70
70
 
71
71
  根因确认后将本 Work 加入 `works_run` 并清空 `current_work`,返回 diagnosis 和下一 Work:局部修复进入 Tickets/I,公共行为或高风险进入 S/Tickets,缺 seam 进入 R,仍无根因则保持 blocked 或进入 W。
72
72
 
73
+ ## 一次性事件与 CI flake
74
+
75
+ 故障不可再次触发或缺安全复现条件时,读取 <Path>{roots.workflows}/specdev/D-diagnose-bugs/references/incident-evidence.md</Path> 进入 evidence-triage。保持原 D ID、diagnosis/diagnostics 和合法状态;事实收集不等于根因确认,未知解释不写入已确认字段。
76
+
77
+ 涉及并发、时钟、子进程或 CI 易抖动测试时,再读 <Path>{roots.workflows}/specdev/common/rules/test-reliability.md</Path>;核对实际 worker/job 拓扑与资源释放,以显式同步制造竞态,不用无证据的 sleep、宽 timeout、全套串行或重试当根因修复。
78
+
73
79
  ## 脱敏与强制变异证据
74
80
 
75
81
  在日志、报告和重现材料中移除 token、Cookie、认证头、秘密配置、个人数据及机器绝对路径;保留项目相对定位和必要错误形态。原始敏感数据不进入长期工件。
@@ -16,6 +16,10 @@ updated_at: <ISO-8601>
16
16
 
17
17
  只记录可观察现象、受影响对象、严重度和时间范围。
18
18
 
19
+ ### 事件证据调查(无法安全复现时)
20
+
21
+ 分开记录直接观察与来源、时间/版本、矛盾、缺失资料、待验证解释和下一安全检查。保持正式假设表为空,根因与修复合同未确认;捕获物仅使用原 diagnostics。按 <Path>{roots.workflows}/specdev/common/rules/engineering-evidence.md</Path> 在本文件追加 incident 记录,不虚构红灯。
22
+
19
23
  ## 2. 红灯反馈回路
20
24
 
21
25
  - **命令:**
@@ -21,7 +21,7 @@
21
21
 
22
22
  把回路当作需要优化的产品:缩小启动范围和测试目标;断言精确症状而非“没有崩溃”;固定时间、随机、文件系统与网络。记录一次运行耗时和复现率。
23
23
 
24
- 非确定性问题以提高复现率为目标:批量运行、并行、增加压力、缩小时间窗口或注入 sleep,直到失败率稳定到可调试区间。
24
+ 非确定性问题先用 barrier、deferred、可控时钟或独立并发进程建立可观察重叠,再用压力运行补充复现率。定时等待只界定观测预算,不作为断言正确或根因修复的依据;不得以注入 sleep 掩盖缺少同步。
25
25
 
26
26
  ## 最小化
27
27
 
@@ -39,3 +39,5 @@
39
39
  - 秒级或已证明是当前最短可行回路;
40
40
  - Agent 可无人值守运行,或由结构化 HITL 脚本捕获结果;
41
41
  - 最小复现的每个剩余元素都有负载作用。
42
+
43
+ 无法复现时可按 <Path>{roots.workflows}/specdev/D-diagnose-bugs/references/incident-evidence.md</Path> 收集事件事实与下一安全检查;正式假设表和已确认根因保持未就绪。
@@ -30,3 +30,7 @@
30
30
  - 确认根因能让红灯出现和消失;
31
31
  - 其他高排名候选有区分性反证;
32
32
  - 临时探针有唯一前缀、owner 和清理结果。
33
+
34
+ ## 确认边界
35
+
36
+ 事件证据调查只整理观察,不进入本正式假设阶段。确认依赖命中精确症状的真实 red 和区分性实验;编译错误、没选中用例、未落地变异及环境失败无效。资源和竞态探针按 <Path>{roots.workflows}/specdev/common/rules/test-reliability.md</Path> 设计并清理。
@@ -0,0 +1,21 @@
1
+ # 事件证据调查
2
+
3
+ 当故障只发生过一次、暂时无法复现,或缺少安全的运行环境时读取。此分支允许收集事实,不放宽已确认根因的红灯标准。当前 Work 仍为 `specdev/diagnose-bugs`;原 diagnosis 与 diagnostics 承载结果,不新建事故状态或永久知识。
4
+
5
+ ## 可执行的调查
6
+
7
+ 1. 固定事件、时间区间、受影响对象、版本和读权限。按来源定位脱敏日志、trace、变更、配置和相关源码路径,不全面导出生产数据。
8
+ 2. 分开写直接观察、来源之间的矛盾、尚缺证据和待验证解释。只读源码可以帮助确定下一步观测点,但解释不进入“已确认根因”或正式假设实验结果。
9
+ 3. 按信息增益和风险排列下一安全检查。生产插桩、重放有副作用请求、真实数据修改或对外调用需要原动作明确授权;缺少授权不代表可以换一个入口执行。
10
+ 4. 事件材料仍写入 diagnosis 的现象部分与附加事件证据节。`feedback_loop_ready` 保持 false,`red_command/red_evidence` 不伪造,正式假设表保持为空,状态为 reproducing 或 blocked;已确认根因和修复契约明确“尚未确认”。
11
+ 5. 写入 <Path>{roots.workflows}/specdev/common/rules/engineering-evidence.md</Path> 规定的 kind=incident 记录,运行 `--stage diagnosis --engineering incident`。校验通过只证明记录满足条件,不表示 bug 已解决或可以 Ready 实施。
12
+
13
+ ## 转入正常诊断
14
+
15
+ 获得一条确实命中精确症状的已运行红灯回路后,再按 <Path>{roots.workflows}/specdev/D-diagnose-bugs/feedback-loop.md</Path> 最小化并进入正式假设/探针。编译失败、未选中测试或基础设施失败不是红灯;临时 probe 要证明落在真实路径并恢复。
16
+
17
+ 反馈回路与确认实验齐全前,不设置 root-cause-confirmed,不移交声称已确认的 Ready 修复。收集完事件材料不把 D 记成根因诊断成功;保留 current_work、缺失证据和恢复入口。用户选择终止调查时按原取消/交接协议处理,不伪造 works_run 或重新打开归档来源。
18
+
19
+ ## 示例
20
+
21
+ 部署后出现一次重复扣款:可以检查脱敏请求 ID、时间、部署 diff 和幂等实现,提出“先核对支付方回执”的安全检查;不能在真实支付端重放以求红灯,也不能仅凭某个可疑分支宣称它就是根因。重复端口错误的 CI 则先核对 worker/job 拓扑和资源分配,使用 <Path>{roots.workflows}/specdev/common/rules/test-reliability.md</Path> 建立确定性观察,不用扩大 sleep 代替原因。
@@ -15,6 +15,12 @@ keywords: [grill, 设计树, 领域, 决策, 共识]
15
15
 
16
16
  读取 `<Path>{roots.workflows}/specdev/common/rules/activation-and-memory.md</Path>`;定位当前 change 的 `<Path>{roots.state}/specdev/changes/{change}/design-tree.json</Path>`、`<Path>{roots.state}/specdev/changes/{change}/LOG.md</Path>`、`<Path>{roots.state}/specdev/changes/{change}/CONTEXT.md</Path>`、`<Path>{roots.state}/specdev/changes/{change}/ADR.md</Path>` 和相关 initiative 决策。先找到相关条目再回读必要原文,不默认整读永久 ADR 或索引。
17
17
 
18
+ ## 按风险选择问题
19
+
20
+ 涉及失败、重试、取消或恢复时读取 <Path>{roots.workflows}/specdev/common/rules/error-handling.md</Path>:谁拥有状态,何时算成功,超时后副作用是否未知,谁核实结果,什么可以重复,哪些依赖应停止?涉及工具/prompt/context 时再读取 <Path>{roots.workflows}/specdev/common/rules/agent-experience.md</Path>。问题由当前 change 风险触发,不固定全问;仓库能查明的事实先查,高影响取舍才询问用户。
21
+
22
+ 局部 ADR 保留真实问题、采纳决定、考虑过的替代方案、代价、适用范围和验证义务。未确认方案留 LOG/设计树,不编造备选,也不把已采纳但未实现的决定写成已交付。
23
+
18
24
  ## 当前 change 的访谈与共识
19
25
 
20
26
  进入访谈必须读取 `<Path>{roots.workflows}/specdev/G-grill-with-docs/references/interview-procedure.md</Path>`,由该过程在出现设计取舍、术语变化、日志写入或外部 stakeholder 缺口时分别读取 grilling、领域建模、日志格式或 questionnaire;不默认加载所有参考。保留完整 frontier、推荐答案和逐轮共识检查,但不替用户回答高影响取舍。