@haaaiawd/loom 1.3.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -86
- package/CONTRIBUTING.md +37 -0
- package/EVIL_EVAL.md +112 -0
- package/README.md +193 -445
- package/README.zh-CN.md +174 -0
- package/SECURITY.md +11 -0
- package/cli/bin/loom.js +171 -998
- package/cli/src/protocol.js +367 -0
- package/cli/src/store.js +626 -0
- package/design.md +194 -0
- package/docs/PROMPT_CATALOG.md +99 -0
- package/docs/RELEASE_CHECKLIST.md +53 -0
- package/docs/UX_FLOW.md +171 -0
- package/docs/brand/loom-mark.svg +18 -0
- package/docs/brand/loom-readme-header.svg +34 -0
- package/docs/brand/loom-readme-header.zh-CN.svg +29 -0
- package/docs/loom-eval-loop.drawio +21 -0
- package/docs/loom-eval-loop.svg +56 -0
- package/docs/loom-production-loop.drawio +41 -0
- package/docs/loom-production-loop.svg +92 -0
- package/package.json +43 -40
- package/EXTERNAL_ACQUISITION_DESIGN.md +0 -143
- package/cli/help/asset.md +0 -36
- package/cli/help/atelier.md +0 -37
- package/cli/help/atlas.md +0 -48
- package/cli/help/capability.md +0 -118
- package/cli/help/concepts.md +0 -105
- package/cli/help/doctor.md +0 -80
- package/cli/help/expertise.md +0 -52
- package/cli/help/loop.md +0 -134
- package/cli/help/patch.md +0 -33
- package/cli/help/proposals.md +0 -21
- package/cli/help/version.md +0 -136
- package/cli/help/workflow.md +0 -116
- package/cli/src/activate.js +0 -505
- package/cli/src/asset-library.js +0 -384
- package/cli/src/atelier.js +0 -331
- package/cli/src/atlas.js +0 -282
- package/cli/src/auto.js +0 -116
- package/cli/src/capability-graph.js +0 -724
- package/cli/src/capability-proposals.js +0 -225
- package/cli/src/diagnostics.js +0 -859
- package/cli/src/expertise-pack.js +0 -336
- package/cli/src/guide.js +0 -548
- package/cli/src/help.js +0 -41
- package/cli/src/init.js +0 -187
- package/cli/src/intent-draft.js +0 -303
- package/cli/src/intent-map.js +0 -747
- package/cli/src/patch.js +0 -214
- package/cli/src/philosophy.js +0 -331
- package/cli/src/shared/intent-ref.js +0 -38
- package/cli/src/shared/md-utils.js +0 -125
- package/cli/src/shared/paths.js +0 -73
- package/cli/src/shared/proof-reference.js +0 -19
- package/cli/src/shared/verification-method.js +0 -32
- package/cli/src/verify.js +0 -394
- package/cli/src/version.js +0 -134
- package/dimensions/AUTHORSHIP.md +0 -45
- package/dimensions/PART_DECOMPOSITION.md +0 -42
- package/dimensions/SEARCH_METHODOLOGY.md +0 -101
- package/dimensions/examples/AGENT_SYSTEM/README.md +0 -219
- package/dimensions/examples/CLI_TOOL/README.md +0 -163
- package/dimensions/universal/COLLABORATION_PHILOSOPHY.md +0 -28
- package/dimensions/universal/ENGINEERING_CREED.md +0 -30
- package/dimensions/universal/PRODUCT_PHILOSOPHY.md +0 -32
- package/meta/BASELINE.md +0 -91
- package/meta/INTENT_LOOP.md +0 -296
- package/meta/PHILOSOPHY_WEAVER.md +0 -110
- package/meta/ROLE_ACTIVATION.md +0 -114
- package/roles/architect.md +0 -92
- package/roles/forge.md +0 -110
- package/roles/impact-reviewer.md +0 -37
- package/roles/keeper.md +0 -113
- package/roles/visionary.md +0 -57
- package/templates/ASSET_LIBRARY_MANIFEST_TEMPLATE.json +0 -10
- package/templates/ATELIER_RECORD_TEMPLATE.json +0 -48
- package/templates/ATLAS_TEMPLATE.html +0 -104
- package/templates/CAPABILITY_BRIEF_TEMPLATE.md +0 -36
- package/templates/CAPABILITY_GRAPH_EXAMPLE.json +0 -188
- package/templates/CAPABILITY_GRAPH_TEMPLATE.json +0 -78
- package/templates/EXPERTISE_PACK_TEMPLATE.json +0 -22
- package/templates/INTENT_MAP_TEMPLATE.json +0 -85
- package/templates/PHILOSOPHY_TEMPLATE.md +0 -44
- package/templates/VISION_TEMPLATE.md +0 -44
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
// capability-proposals — auditable incoming change, deliberately separate from the official graph.
|
|
2
|
-
|
|
3
|
-
import { createHash } from 'node:crypto';
|
|
4
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
5
|
-
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
6
|
-
import { loadCapabilityGraph } from './capability-graph.js';
|
|
7
|
-
import { loadIntentMap } from './intent-map.js';
|
|
8
|
-
import { readJsonFile } from './shared/md-utils.js';
|
|
9
|
-
|
|
10
|
-
const DIR = '07_GRAPH_PROPOSALS';
|
|
11
|
-
const ORIGINS = ['user_request', 'research', 'keeper_finding', 'forge_finding'];
|
|
12
|
-
const CANDIDATES = ['outcome', 'concern', 'capability', 'risk', 'evidence', 'constraint'];
|
|
13
|
-
const DECISIONS = ['covered', 'graph_update', 'intent_change', 'acceptance_change', 'minor', 'major', 'reject'];
|
|
14
|
-
const DECISION_ARTIFACT_DIR = '03_DECISIONS';
|
|
15
|
-
|
|
16
|
-
export function getCapabilityProposalsDir(versionDir) { return join(versionDir, DIR); }
|
|
17
|
-
|
|
18
|
-
function proposalPath(versionDir, id) {
|
|
19
|
-
if (!/^CGP-[A-Z0-9][A-Z0-9-]*$/.test(id || '')) throw new Error('proposal id must use CGP-<UPPERCASE-ID>');
|
|
20
|
-
return join(getCapabilityProposalsDir(versionDir), `${id}.json`);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function text(value, name, errors) { if (typeof value !== 'string' || !value.trim()) errors.push(`${name} must be a non-empty string`); }
|
|
24
|
-
|
|
25
|
-
function fileHash(path) {
|
|
26
|
-
return existsSync(path) && statSync(path).isFile()
|
|
27
|
-
? createHash('sha256').update(readFileSync(path)).digest('hex')
|
|
28
|
-
: null;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function directoryHash(path) {
|
|
32
|
-
if (!existsSync(path)) return null;
|
|
33
|
-
const entries = readdirSync(path, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
34
|
-
const digest = createHash('sha256');
|
|
35
|
-
for (const entry of entries) {
|
|
36
|
-
const child = join(path, entry.name);
|
|
37
|
-
digest.update(entry.name);
|
|
38
|
-
digest.update(entry.isDirectory() ? directoryHash(child) || '' : fileHash(child) || '');
|
|
39
|
-
}
|
|
40
|
-
return digest.digest('hex');
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function acceptanceHash(versionDir) {
|
|
44
|
-
const mapPath = join(versionDir, '04_INTENT_MAP.json');
|
|
45
|
-
const verificationPath = join(versionDir, '05_VERIFICATION.md');
|
|
46
|
-
let inline = null;
|
|
47
|
-
if (existsSync(mapPath)) {
|
|
48
|
-
try {
|
|
49
|
-
const map = JSON.parse(readFileSync(mapPath, 'utf-8'));
|
|
50
|
-
inline = Object.fromEntries(Object.entries(map.intents || {}).map(([id, intent]) => [id, intent.acceptance]));
|
|
51
|
-
} catch { inline = null; }
|
|
52
|
-
}
|
|
53
|
-
return createHash('sha256').update(JSON.stringify(inline)).update(fileHash(verificationPath) || '').digest('hex');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function snapshot(versionDir) {
|
|
57
|
-
return {
|
|
58
|
-
graph: fileHash(join(versionDir, '07_CAPABILITY_GRAPH.json')),
|
|
59
|
-
intent_map: fileHash(join(versionDir, '04_INTENT_MAP.json')),
|
|
60
|
-
acceptance: acceptanceHash(versionDir),
|
|
61
|
-
decisions: directoryHash(join(versionDir, DECISION_ARTIFACT_DIR)),
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function nonEmptyStringArray(value, label, errors) {
|
|
66
|
-
if (!Array.isArray(value) || value.length === 0 || value.some((item) => typeof item !== 'string' || !item.trim())) errors.push(`${label} must be a non-empty string array`);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function validateResolutionShape(resolution, decision, errors) {
|
|
70
|
-
if (!resolution || typeof resolution !== 'object' || Array.isArray(resolution)) {
|
|
71
|
-
errors.push('resolution must be an object');
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
if (decision === 'graph_update') {
|
|
75
|
-
if (!resolution.graph || typeof resolution.graph !== 'object' || Array.isArray(resolution.graph)) errors.push('resolution.graph is required for graph_update');
|
|
76
|
-
else nonEmptyStringArray(resolution.graph.node_ids, 'resolution.graph.node_ids', errors);
|
|
77
|
-
}
|
|
78
|
-
if (decision === 'intent_change') nonEmptyStringArray(resolution.intent_ids, 'resolution.intent_ids', errors);
|
|
79
|
-
if (decision === 'acceptance_change') nonEmptyStringArray(resolution.acceptance_intent_ids, 'resolution.acceptance_intent_ids', errors);
|
|
80
|
-
if (decision === 'covered') {
|
|
81
|
-
if (!resolution.coverage || typeof resolution.coverage !== 'object' || Array.isArray(resolution.coverage)) errors.push('resolution.coverage is required for covered');
|
|
82
|
-
else {
|
|
83
|
-
nonEmptyStringArray(resolution.coverage.node_ids, 'resolution.coverage.node_ids', errors);
|
|
84
|
-
text(resolution.coverage.rationale, 'resolution.coverage.rationale', errors);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
if (['minor', 'major', 'reject'].includes(decision)) text(resolution.decision_artifact_ref, 'resolution.decision_artifact_ref', errors);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function validateCapabilityProposal(proposal) {
|
|
91
|
-
const errors = [];
|
|
92
|
-
text(proposal?.id, 'id', errors);
|
|
93
|
-
if (!ORIGINS.includes(proposal?.origin)) errors.push(`origin must be one of ${ORIGINS.join(', ')}`);
|
|
94
|
-
if (!CANDIDATES.includes(proposal?.candidate_kind)) errors.push(`candidate_kind must be one of ${CANDIDATES.join(', ')}`);
|
|
95
|
-
text(proposal?.title, 'title', errors);
|
|
96
|
-
text(proposal?.why_now, 'why_now', errors);
|
|
97
|
-
if (!proposal?.provenance || typeof proposal.provenance !== 'object') errors.push('provenance is required');
|
|
98
|
-
else {
|
|
99
|
-
text(proposal.provenance.source, 'provenance.source', errors);
|
|
100
|
-
text(proposal.provenance.observed_at, 'provenance.observed_at', errors);
|
|
101
|
-
text(proposal.provenance.evidence, 'provenance.evidence', errors);
|
|
102
|
-
}
|
|
103
|
-
if (!['submitted', 'decided', 'closed'].includes(proposal?.status)) errors.push('status must be submitted, decided, or closed');
|
|
104
|
-
if (proposal?.status !== 'submitted') {
|
|
105
|
-
if (!DECISIONS.includes(proposal?.decision)) errors.push(`decision must be one of ${DECISIONS.join(', ')}`);
|
|
106
|
-
text(proposal?.decision_rationale, 'decision_rationale', errors);
|
|
107
|
-
if (!proposal.decision_baseline || typeof proposal.decision_baseline !== 'object') errors.push('decision_baseline is required after Architect decision');
|
|
108
|
-
}
|
|
109
|
-
if (proposal?.status === 'closed') validateResolutionShape(proposal.resolution, proposal.decision, errors);
|
|
110
|
-
if (errors.length) throw new Error(`Capability Graph proposal validation failed:\n - ${errors.join('\n - ')}`);
|
|
111
|
-
return proposal;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function listCapabilityProposals(versionDir, { unresolvedOnly = false } = {}) {
|
|
115
|
-
const dir = getCapabilityProposalsDir(versionDir);
|
|
116
|
-
if (!existsSync(dir)) return [];
|
|
117
|
-
const proposals = readdirSync(dir).filter((file) => file.endsWith('.json')).sort()
|
|
118
|
-
.map((file) => validateCapabilityProposal(readJsonFile(join(dir, file), 'Capability Graph proposal')));
|
|
119
|
-
return unresolvedOnly ? proposals.filter((proposal) => proposal.status !== 'closed') : proposals;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export function getCapabilityProposal(versionDir, id) {
|
|
123
|
-
const path = proposalPath(versionDir, id);
|
|
124
|
-
if (!existsSync(path)) throw new Error(`proposal not found: ${id}`);
|
|
125
|
-
return validateCapabilityProposal(readJsonFile(path, 'Capability Graph proposal'));
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export function submitCapabilityProposal(versionDir, proposal) {
|
|
129
|
-
validateCapabilityProposal(proposal);
|
|
130
|
-
if (proposal.status !== 'submitted') throw new Error('new proposal must start as status=submitted');
|
|
131
|
-
const path = proposalPath(versionDir, proposal.id);
|
|
132
|
-
if (existsSync(path)) throw new Error(`proposal already exists: ${proposal.id}`);
|
|
133
|
-
mkdirSync(getCapabilityProposalsDir(versionDir), { recursive: true });
|
|
134
|
-
writeFileSync(path, `${JSON.stringify(proposal, null, 2)}\n`, 'utf-8');
|
|
135
|
-
return proposal;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export function decideCapabilityProposal(versionDir, id, decision, rationale) {
|
|
139
|
-
if (!DECISIONS.includes(decision)) throw new Error(`invalid decision: ${decision}`);
|
|
140
|
-
if (!rationale || !rationale.trim()) throw new Error('--rationale is required');
|
|
141
|
-
const proposal = getCapabilityProposal(versionDir, id);
|
|
142
|
-
if (proposal.status !== 'submitted') throw new Error(`${id} is already ${proposal.status}; it cannot be decided again`);
|
|
143
|
-
proposal.status = 'decided';
|
|
144
|
-
proposal.decision = decision;
|
|
145
|
-
proposal.decision_rationale = rationale;
|
|
146
|
-
proposal.decision_baseline = snapshot(versionDir);
|
|
147
|
-
writeFileSync(proposalPath(versionDir, id), `${JSON.stringify(proposal, null, 2)}\n`, 'utf-8');
|
|
148
|
-
return proposal;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function isEffectiveCoverage(node) {
|
|
152
|
-
return node?.status === 'covered' && ['brief', 'intent', 'defer', 'exclude', 'covered_by'].includes(node.route);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function assertGraphResolution(versionDir, proposal, resolution) {
|
|
156
|
-
const graph = loadCapabilityGraph(versionDir);
|
|
157
|
-
if (snapshot(versionDir).graph === proposal.decision_baseline.graph) throw new Error('graph_update requires a real change to 07_CAPABILITY_GRAPH.json after the Architect decision');
|
|
158
|
-
for (const nodeId of resolution.graph.node_ids) {
|
|
159
|
-
const node = graph.nodes[nodeId];
|
|
160
|
-
if (!node) throw new Error(`resolution.graph.node_ids references missing Graph node: ${nodeId}`);
|
|
161
|
-
if (!(node.proposal_refs || []).includes(proposal.id)) throw new Error(`Graph node ${nodeId} must record proposal_refs including ${proposal.id}`);
|
|
162
|
-
}
|
|
163
|
-
if (proposal.candidate_kind === 'constraint') {
|
|
164
|
-
const carrier = (graph.constraints || []).find((constraint) => constraint.proposal_id === proposal.id);
|
|
165
|
-
if (!carrier) throw new Error(`constraint proposal ${proposal.id} requires a formal Graph constraints carrier`);
|
|
166
|
-
if (!carrier.node_refs.some((nodeId) => resolution.graph.node_ids.includes(nodeId))) throw new Error(`constraint carrier ${proposal.id} must reference a resolved Graph node`);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function assertIntentResolution(versionDir, proposal, resolution, { acceptance = false } = {}) {
|
|
171
|
-
const current = snapshot(versionDir);
|
|
172
|
-
const baselineKey = acceptance ? 'acceptance' : 'intent_map';
|
|
173
|
-
if (current[baselineKey] === proposal.decision_baseline[baselineKey]) {
|
|
174
|
-
throw new Error(`${proposal.decision} requires a real ${acceptance ? 'acceptance contract' : '04_INTENT_MAP.json'} change after the Architect decision`);
|
|
175
|
-
}
|
|
176
|
-
const map = loadIntentMap(versionDir);
|
|
177
|
-
const intentIds = acceptance ? resolution.acceptance_intent_ids : resolution.intent_ids;
|
|
178
|
-
for (const intentId of intentIds) {
|
|
179
|
-
const intent = map.intents[intentId];
|
|
180
|
-
if (!intent) throw new Error(`resolution references missing Intent: ${intentId}`);
|
|
181
|
-
if (!(intent.proposal_refs || []).includes(proposal.id)) throw new Error(`Intent ${intentId} must record proposal_refs including ${proposal.id}`);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function resolveDecisionArtifact(versionDir, reference) {
|
|
186
|
-
if (typeof reference !== 'string' || !reference.trim() || isAbsolute(reference)) throw new Error('resolution.decision_artifact_ref must be a relative decision artifact path');
|
|
187
|
-
const decisionsRoot = resolve(versionDir, DECISION_ARTIFACT_DIR);
|
|
188
|
-
const artifactPath = resolve(versionDir, reference);
|
|
189
|
-
const relation = relative(decisionsRoot, artifactPath);
|
|
190
|
-
if (relation.startsWith('..') || isAbsolute(relation) || relation === '') throw new Error(`decision artifact must be inside ${DECISION_ARTIFACT_DIR}/`);
|
|
191
|
-
if (!existsSync(artifactPath) || !statSync(artifactPath).isFile()) throw new Error(`decision artifact does not exist: ${reference}`);
|
|
192
|
-
return artifactPath;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function assertDecisionArtifactResolution(versionDir, proposal, resolution) {
|
|
196
|
-
const artifactPath = resolveDecisionArtifact(versionDir, resolution.decision_artifact_ref);
|
|
197
|
-
if (snapshot(versionDir).decisions === proposal.decision_baseline.decisions) throw new Error('decision resolution requires a new or changed 03_DECISIONS artifact after the Architect decision');
|
|
198
|
-
if (!readFileSync(artifactPath, 'utf-8').includes(proposal.id)) throw new Error(`decision artifact must name proposal ${proposal.id}`);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function assertCoveredResolution(versionDir, resolution) {
|
|
202
|
-
const graph = loadCapabilityGraph(versionDir);
|
|
203
|
-
for (const nodeId of resolution.coverage.node_ids) {
|
|
204
|
-
if (!isEffectiveCoverage(graph.nodes[nodeId])) throw new Error(`coverage node is not an effective current Graph coverage: ${nodeId}`);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
export function closeCapabilityProposal(versionDir, id, resolution) {
|
|
209
|
-
const proposal = getCapabilityProposal(versionDir, id);
|
|
210
|
-
if (proposal.status !== 'decided') throw new Error(`${id} must be decided before it can be closed`);
|
|
211
|
-
const errors = [];
|
|
212
|
-
validateResolutionShape(resolution, proposal.decision, errors);
|
|
213
|
-
if (errors.length) throw new Error(`proposal close requires structured resolution:\n - ${errors.join('\n - ')}`);
|
|
214
|
-
|
|
215
|
-
if (proposal.decision === 'graph_update') assertGraphResolution(versionDir, proposal, resolution);
|
|
216
|
-
else if (proposal.decision === 'intent_change') assertIntentResolution(versionDir, proposal, resolution);
|
|
217
|
-
else if (proposal.decision === 'acceptance_change') assertIntentResolution(versionDir, proposal, resolution, { acceptance: true });
|
|
218
|
-
else if (proposal.decision === 'covered') assertCoveredResolution(versionDir, resolution);
|
|
219
|
-
else assertDecisionArtifactResolution(versionDir, proposal, resolution);
|
|
220
|
-
|
|
221
|
-
proposal.status = 'closed';
|
|
222
|
-
proposal.resolution = resolution;
|
|
223
|
-
writeFileSync(proposalPath(versionDir, id), `${JSON.stringify(proposal, null, 2)}\n`, 'utf-8');
|
|
224
|
-
return proposal;
|
|
225
|
-
}
|