@planu/cli 4.8.0 → 4.10.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 +22 -0
- package/dist/config/license-plans.json +5 -0
- package/dist/config/project-knowledge-graph.json +70 -2
- package/dist/config/registries/hosts/codex.json +3 -4
- package/dist/engine/evidence-index/index-builder.js +38 -12
- package/dist/engine/project-graph/builder.js +9 -5
- package/dist/engine/project-graph/cache.js +4 -0
- package/dist/engine/project-graph/extractors/decision-store-extractor.d.ts +3 -0
- package/dist/engine/project-graph/extractors/decision-store-extractor.js +57 -0
- package/dist/engine/structural-memory/architecture-snapshot.d.ts +3 -0
- package/dist/engine/structural-memory/architecture-snapshot.js +132 -0
- package/dist/engine/structural-memory/artifact-policy.d.ts +5 -0
- package/dist/engine/structural-memory/artifact-policy.js +17 -0
- package/dist/engine/structural-memory/change-impact.d.ts +3 -0
- package/dist/engine/structural-memory/change-impact.js +159 -0
- package/dist/engine/structural-memory/contracts.d.ts +2 -0
- package/dist/engine/structural-memory/contracts.js +2 -0
- package/dist/engine/structural-memory/helpers.d.ts +28 -0
- package/dist/engine/structural-memory/helpers.js +122 -0
- package/dist/engine/structural-memory/index.d.ts +7 -0
- package/dist/engine/structural-memory/index.js +6 -0
- package/dist/engine/structural-memory/query.d.ts +3 -0
- package/dist/engine/structural-memory/query.js +67 -0
- package/dist/engine/structural-memory/status.d.ts +3 -0
- package/dist/engine/structural-memory/status.js +30 -0
- package/dist/storage/feedback-remote.d.ts +5 -1
- package/dist/storage/feedback-remote.js +75 -16
- package/dist/storage/feedback-store.d.ts +3 -1
- package/dist/storage/feedback-store.js +107 -10
- package/dist/tools/feedback-handler.d.ts +3 -1
- package/dist/tools/feedback-handler.js +47 -1
- package/dist/tools/register-sdd-tools.d.ts +1 -1
- package/dist/tools/register-sdd-tools.js +5 -0
- package/dist/tools/status-handler.js +11 -11
- package/dist/tools/tool-registry/core-tools.js +97 -0
- package/dist/tools/tool-registry.js +3 -0
- package/dist/tools/validate-assurance.d.ts +6 -0
- package/dist/tools/validate-assurance.js +55 -0
- package/dist/tools/validate-graph-coverage.d.ts +8 -0
- package/dist/tools/validate-graph-coverage.js +30 -0
- package/dist/tools/validate-helpers.d.ts +9 -0
- package/dist/tools/validate-helpers.js +47 -0
- package/dist/tools/validate-lint.d.ts +3 -0
- package/dist/tools/validate-lint.js +67 -0
- package/dist/tools/validate-minimality.d.ts +16 -0
- package/dist/tools/validate-minimality.js +22 -0
- package/dist/tools/validate-runtime.d.ts +14 -0
- package/dist/tools/validate-runtime.js +85 -0
- package/dist/tools/validate.js +5 -222
- package/dist/types/evidence-index.d.ts +1 -0
- package/dist/types/feedback.d.ts +37 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +2 -0
- package/dist/types/project-knowledge-graph.d.ts +10 -0
- package/dist/types/structural-memory.d.ts +138 -0
- package/dist/types/structural-memory.js +2 -0
- package/package.json +20 -20
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
## [4.10.0] - 2026-07-01
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
- feat(SPEC-1100): add reliable feedback sync
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
- fix(SPEC-1102): honor release preflight test skip
|
|
8
|
+
- fix(SPEC-1101): harden validate runtime and status
|
|
9
|
+
|
|
10
|
+
### Chores
|
|
11
|
+
- chore(deps): refresh direct dependencies
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## [4.9.0] - 2026-06-30
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
- feat(SPEC-1099): add structural memory layer
|
|
18
|
+
|
|
19
|
+
### Chores
|
|
20
|
+
- chore(planu): clear released pending specs
|
|
21
|
+
|
|
22
|
+
|
|
1
23
|
## [4.8.0] - 2026-06-24
|
|
2
24
|
|
|
3
25
|
### Features
|
|
@@ -92,6 +92,7 @@
|
|
|
92
92
|
"export_spec",
|
|
93
93
|
"expose_spec_as_prompt",
|
|
94
94
|
"facilitate",
|
|
95
|
+
"feedback_status",
|
|
95
96
|
"gc_data_projects",
|
|
96
97
|
"generate_api_client",
|
|
97
98
|
"generate_attestation",
|
|
@@ -163,6 +164,7 @@
|
|
|
163
164
|
"render_spec_for_provider",
|
|
164
165
|
"repair_frontmatter_drift",
|
|
165
166
|
"request_changes",
|
|
167
|
+
"resolve_feedback",
|
|
166
168
|
"reverse_engineer",
|
|
167
169
|
"rewrite_criteria_ears",
|
|
168
170
|
"run_mutation_hints",
|
|
@@ -189,13 +191,16 @@
|
|
|
189
191
|
"suggest_token_optimizer",
|
|
190
192
|
"suggest_tooling",
|
|
191
193
|
"summarize_spec",
|
|
194
|
+
"submit_feedback",
|
|
192
195
|
"sync_ai_configs",
|
|
196
|
+
"sync_feedback",
|
|
193
197
|
"sync_spec_state",
|
|
194
198
|
"tdd_scaffold",
|
|
195
199
|
"tdd_status",
|
|
196
200
|
"telemetry_health",
|
|
197
201
|
"token_optimizer_status",
|
|
198
202
|
"transform_code",
|
|
203
|
+
"triage_feedback",
|
|
199
204
|
"triage_request",
|
|
200
205
|
"type_safety_gate",
|
|
201
206
|
"unregister_project_path",
|
|
@@ -18,7 +18,14 @@
|
|
|
18
18
|
"tool",
|
|
19
19
|
"handoff",
|
|
20
20
|
"validation",
|
|
21
|
-
"commit"
|
|
21
|
+
"commit",
|
|
22
|
+
"adr",
|
|
23
|
+
"validation_evidence",
|
|
24
|
+
"release_risk",
|
|
25
|
+
"architecture_risk",
|
|
26
|
+
"drift",
|
|
27
|
+
"contract",
|
|
28
|
+
"git_change"
|
|
22
29
|
],
|
|
23
30
|
"edgeTypes": [
|
|
24
31
|
"contains",
|
|
@@ -31,7 +38,13 @@
|
|
|
31
38
|
"has_risk",
|
|
32
39
|
"uses_tool",
|
|
33
40
|
"mentions",
|
|
34
|
-
"neighbors"
|
|
41
|
+
"neighbors",
|
|
42
|
+
"affects",
|
|
43
|
+
"covers",
|
|
44
|
+
"depends_on",
|
|
45
|
+
"has_evidence",
|
|
46
|
+
"has_status",
|
|
47
|
+
"has_contract"
|
|
35
48
|
],
|
|
36
49
|
"classifications": ["extracted", "inferred", "ambiguous"],
|
|
37
50
|
"confidenceLabels": ["high", "medium", "low"],
|
|
@@ -52,6 +65,61 @@
|
|
|
52
65
|
"(api[_-]?key|token|secret|password)\\s*[:=]\\s*[^\\s,;]+"
|
|
53
66
|
]
|
|
54
67
|
},
|
|
68
|
+
"structuralMemory": {
|
|
69
|
+
"schemaVersion": "1.0.0",
|
|
70
|
+
"nodeTypes": [
|
|
71
|
+
"spec",
|
|
72
|
+
"criterion",
|
|
73
|
+
"file",
|
|
74
|
+
"test",
|
|
75
|
+
"adr",
|
|
76
|
+
"decision",
|
|
77
|
+
"validation_evidence",
|
|
78
|
+
"validation",
|
|
79
|
+
"release",
|
|
80
|
+
"release_risk",
|
|
81
|
+
"architecture_risk",
|
|
82
|
+
"drift",
|
|
83
|
+
"risk",
|
|
84
|
+
"tool",
|
|
85
|
+
"contract",
|
|
86
|
+
"git_change"
|
|
87
|
+
],
|
|
88
|
+
"edgeTypes": [
|
|
89
|
+
"contains",
|
|
90
|
+
"references",
|
|
91
|
+
"implements",
|
|
92
|
+
"tests",
|
|
93
|
+
"validated_by",
|
|
94
|
+
"decides",
|
|
95
|
+
"has_risk",
|
|
96
|
+
"uses_tool",
|
|
97
|
+
"affects",
|
|
98
|
+
"covers",
|
|
99
|
+
"depends_on",
|
|
100
|
+
"has_evidence",
|
|
101
|
+
"has_status",
|
|
102
|
+
"has_contract"
|
|
103
|
+
],
|
|
104
|
+
"response": {
|
|
105
|
+
"defaultLimit": 60,
|
|
106
|
+
"maxLimit": 200
|
|
107
|
+
},
|
|
108
|
+
"propertySchemas": {
|
|
109
|
+
"spec": ["specId", "status"],
|
|
110
|
+
"criterion": ["specId", "index"],
|
|
111
|
+
"file": ["path"],
|
|
112
|
+
"test": ["path"],
|
|
113
|
+
"decision": ["specId", "decisionId"],
|
|
114
|
+
"adr": ["specId", "decisionId"],
|
|
115
|
+
"validation_evidence": ["specId", "kind", "status"],
|
|
116
|
+
"release_risk": ["specId", "severity"],
|
|
117
|
+
"architecture_risk": ["path", "ruleId", "severity"],
|
|
118
|
+
"drift": ["specId", "status", "severity"],
|
|
119
|
+
"contract": ["kind", "passed"],
|
|
120
|
+
"git_change": ["path", "status"]
|
|
121
|
+
}
|
|
122
|
+
},
|
|
55
123
|
"toolNamePatterns": [
|
|
56
124
|
"package_handoff",
|
|
57
125
|
"validate",
|
|
@@ -294,6 +294,8 @@
|
|
|
294
294
|
"submit_feedback",
|
|
295
295
|
"triage_feedback",
|
|
296
296
|
"resolve_feedback",
|
|
297
|
+
"sync_feedback",
|
|
298
|
+
"feedback_status",
|
|
297
299
|
"delete_spec",
|
|
298
300
|
"delete_project",
|
|
299
301
|
"delete_pattern",
|
|
@@ -373,10 +375,7 @@
|
|
|
373
375
|
"analyze_performance_impact",
|
|
374
376
|
"recommend_model"
|
|
375
377
|
],
|
|
376
|
-
"unsupportedTools": [
|
|
377
|
-
"challenge_spec",
|
|
378
|
-
"red_team"
|
|
379
|
-
],
|
|
378
|
+
"unsupportedTools": ["challenge_spec", "red_team"],
|
|
380
379
|
"restrictedTools": {
|
|
381
380
|
"challenge_spec": "Requires interactive UI — use planu_facilitate instead",
|
|
382
381
|
"approve_spec": "Requires reviewer token — use planu_facilitate instead"
|
|
@@ -37,16 +37,16 @@ async function collectEvidence(args) {
|
|
|
37
37
|
records.push(record('scenario', row.scenario, 'valid', 'traceability-matrix'));
|
|
38
38
|
}
|
|
39
39
|
for (const test of row.testEvidence ?? []) {
|
|
40
|
-
records.push(await pathRecord('test', test, 'traceability-matrix', args));
|
|
40
|
+
records.push(await pathRecord('test', test, 'traceability-matrix', args, 'testEvidence'));
|
|
41
41
|
}
|
|
42
42
|
for (const contract of row.contractEvidence ?? []) {
|
|
43
|
-
records.push(await pathRecord('contract', contract, 'traceability-matrix', args));
|
|
43
|
+
records.push(await pathRecord('contract', contract, 'traceability-matrix', args, 'contractEvidence'));
|
|
44
44
|
}
|
|
45
45
|
if (row.manualEvidence) {
|
|
46
46
|
records.push(record('manual', row.manualEvidence, 'valid', 'traceability-matrix'));
|
|
47
47
|
}
|
|
48
48
|
for (const changedFile of row.changedFiles) {
|
|
49
|
-
records.push(await pathRecord('changed-file', changedFile, 'traceability-matrix', args));
|
|
49
|
+
records.push(await pathRecord('changed-file', changedFile, 'traceability-matrix', args, 'changedFiles'));
|
|
50
50
|
}
|
|
51
51
|
if (row.validationEvidence) {
|
|
52
52
|
records.push(validationRecord(row.validationEvidence));
|
|
@@ -57,7 +57,7 @@ async function collectEvidence(args) {
|
|
|
57
57
|
}
|
|
58
58
|
for (const validation of args.contractValidations) {
|
|
59
59
|
if (validation.reportPath) {
|
|
60
|
-
records.push(await pathRecord('contract', validation.reportPath, `contract-validation:${validation.kind}`, args));
|
|
60
|
+
records.push(await pathRecord('contract', validation.reportPath, `contract-validation:${validation.kind}`, args, 'contractValidation.reportPath'));
|
|
61
61
|
}
|
|
62
62
|
if (validation.summary) {
|
|
63
63
|
records.push({
|
|
@@ -95,36 +95,62 @@ function parseValidationEvidence(value) {
|
|
|
95
95
|
return { command: value };
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
|
-
async function pathRecord(kind, value, source, args) {
|
|
99
|
-
const safe = resolveSafeProjectPath(value, args.projectPath);
|
|
98
|
+
async function pathRecord(kind, value, source, args, fieldName) {
|
|
99
|
+
const safe = resolveSafeProjectPath(value, args.projectPath, fieldName);
|
|
100
100
|
if (!safe.ok) {
|
|
101
101
|
args.diagnostics.push(`${value}: ${safe.reason}`);
|
|
102
102
|
return record(kind, value, 'invalid', source, safe.reason);
|
|
103
103
|
}
|
|
104
104
|
if (!args.projectPath) {
|
|
105
|
-
return record(kind, value, 'missing', source, 'projectPath unavailable');
|
|
105
|
+
return record(kind, value, 'missing', source, evidencePathReason(fieldName, 'projectPath unavailable'));
|
|
106
106
|
}
|
|
107
107
|
const exists = await stat(safe.path)
|
|
108
108
|
.then(() => true)
|
|
109
109
|
.catch(() => false);
|
|
110
|
-
|
|
110
|
+
const missingReason = evidencePathReason(fieldName, `Missing path: ${value}`);
|
|
111
|
+
return record(kind, value, exists ? 'valid' : 'stale', source, exists ? undefined : missingReason);
|
|
111
112
|
}
|
|
112
|
-
function resolveSafeProjectPath(value, projectPath) {
|
|
113
|
+
function resolveSafeProjectPath(value, projectPath, fieldName) {
|
|
113
114
|
if (value.trim().length === 0) {
|
|
114
|
-
return { ok: false, reason: '
|
|
115
|
+
return { ok: false, reason: evidencePathReason(fieldName, 'Empty path.') };
|
|
116
|
+
}
|
|
117
|
+
if (looksLikeShellCommand(value)) {
|
|
118
|
+
return { ok: false, reason: evidencePathReason(fieldName, 'Commands are not valid paths.') };
|
|
119
|
+
}
|
|
120
|
+
if (looksLikeGlob(value)) {
|
|
121
|
+
return { ok: false, reason: evidencePathReason(fieldName, 'Globs are not valid paths.') };
|
|
115
122
|
}
|
|
116
123
|
if (isAbsolute(value)) {
|
|
117
|
-
return { ok: false, reason: '
|
|
124
|
+
return { ok: false, reason: evidencePathReason(fieldName, 'Absolute paths are not allowed.') };
|
|
118
125
|
}
|
|
119
126
|
const normalized = normalize(value);
|
|
120
127
|
if (normalized.startsWith('..') || normalized.includes('/../')) {
|
|
121
|
-
return { ok: false, reason: '
|
|
128
|
+
return { ok: false, reason: evidencePathReason(fieldName, 'Path traversal is not allowed.') };
|
|
122
129
|
}
|
|
123
130
|
if (!projectPath) {
|
|
124
131
|
return { ok: true, path: normalized };
|
|
125
132
|
}
|
|
126
133
|
return { ok: true, path: join(projectPath, normalized) };
|
|
127
134
|
}
|
|
135
|
+
function evidencePathReason(fieldName, detail) {
|
|
136
|
+
if (fieldName === 'testEvidence') {
|
|
137
|
+
return `testEvidence must contain existing file paths only; put command output in validationEvidence. ${detail}`;
|
|
138
|
+
}
|
|
139
|
+
if (fieldName === 'changedFiles') {
|
|
140
|
+
return `changedFiles must contain existing repository-relative paths only; no globs or commands. ${detail}`;
|
|
141
|
+
}
|
|
142
|
+
return `Evidence path must be an existing repository-relative path. ${detail}`;
|
|
143
|
+
}
|
|
144
|
+
function looksLikeShellCommand(value) {
|
|
145
|
+
const trimmed = value.trim();
|
|
146
|
+
if (/\s/.test(trimmed) && !/\.[A-Za-z0-9]+$/.test(trimmed)) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
return /^(?:pnpm|npm|yarn|bun|npx|node|tsx|tsc|vitest|jest|git|bash|sh)\b/.test(trimmed);
|
|
150
|
+
}
|
|
151
|
+
function looksLikeGlob(value) {
|
|
152
|
+
return /[*?[\]{}]/.test(value);
|
|
153
|
+
}
|
|
128
154
|
function record(kind, value, status, source, reason) {
|
|
129
155
|
return { kind, value, status, source, ...(reason ? { reason } : {}) };
|
|
130
156
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { collectProjectGraphSources, diffSourceHashes, hashText, loadProjectGraphPolicy, projectGraphCachePath, projectGraphPath, readProjectGraph, readProjectGraphSourceHashes, writeProjectGraph, writeProjectGraphSourceHashes, } from './cache.js';
|
|
2
2
|
import { extractGitGraph, extractReleaseGraph } from './extractors/git-extractor.js';
|
|
3
3
|
import { extractHandoffGraph } from './extractors/handoff-extractor.js';
|
|
4
|
+
import { extractDecisionStoreGraph } from './extractors/decision-store-extractor.js';
|
|
4
5
|
import { extractSpecGraph } from './extractors/spec-extractor.js';
|
|
5
6
|
import { extractValidationGraph } from './extractors/validation-extractor.js';
|
|
6
7
|
function uniqueNodes(nodes) {
|
|
@@ -11,10 +12,7 @@ function uniqueEdges(edges) {
|
|
|
11
12
|
}
|
|
12
13
|
function validateAgainstPolicy(result, policy) {
|
|
13
14
|
const nodes = result.nodes.filter((node) => policy.nodeTypes.includes(node.type));
|
|
14
|
-
const
|
|
15
|
-
const edges = result.edges.filter((edge) => nodeIds.has(edge.from) &&
|
|
16
|
-
nodeIds.has(edge.to) &&
|
|
17
|
-
policy.edgeTypes.includes(edge.type) &&
|
|
15
|
+
const edges = result.edges.filter((edge) => policy.edgeTypes.includes(edge.type) &&
|
|
18
16
|
policy.confidenceLabels.includes(edge.confidence) &&
|
|
19
17
|
policy.classifications.includes(edge.classification));
|
|
20
18
|
return { nodes, edges };
|
|
@@ -50,6 +48,9 @@ export async function buildProjectKnowledgeGraph(args) {
|
|
|
50
48
|
else if (source.kind === 'handoff') {
|
|
51
49
|
extracted.push(extractHandoffGraph(source, policy));
|
|
52
50
|
}
|
|
51
|
+
else if (source.kind === 'decisions') {
|
|
52
|
+
extracted.push(extractDecisionStoreGraph(source, policy));
|
|
53
|
+
}
|
|
53
54
|
else if (source.kind === 'release-events') {
|
|
54
55
|
extracted.push(extractReleaseGraph(source, policy));
|
|
55
56
|
}
|
|
@@ -57,7 +58,10 @@ export async function buildProjectKnowledgeGraph(args) {
|
|
|
57
58
|
extracted.push(await extractGitGraph({ projectPath: args.projectPath, policy }));
|
|
58
59
|
const validated = extracted.map((result) => validateAgainstPolicy(result, policy));
|
|
59
60
|
const nodes = uniqueNodes(validated.flatMap((result) => result.nodes));
|
|
60
|
-
const
|
|
61
|
+
const nodeIds = new Set(nodes.map((node) => node.id));
|
|
62
|
+
const edges = uniqueEdges(validated
|
|
63
|
+
.flatMap((result) => result.edges)
|
|
64
|
+
.filter((edge) => nodeIds.has(edge.from) && nodeIds.has(edge.to)));
|
|
61
65
|
const sourceHashes = diff.next;
|
|
62
66
|
const sourceHash = hashText(JSON.stringify(sourceHashes));
|
|
63
67
|
const graph = {
|
|
@@ -83,6 +83,7 @@ export async function collectProjectGraphSources(args) {
|
|
|
83
83
|
const specFiles = await collectFiles(join(projectRoot, 'planu', 'specs'), (path) => path.endsWith('/spec.md'));
|
|
84
84
|
const handoffFiles = await collectFiles(join(dataRoot, 'handoffs'), (path) => /\.(json|md)$/.test(path));
|
|
85
85
|
const validationFiles = handoffFiles.filter((path) => path.includes('validation'));
|
|
86
|
+
const decisions = join(dataRoot, 'decisions.json');
|
|
86
87
|
const transitionLog = join(dataRoot, 'transition-log.jsonl');
|
|
87
88
|
const releaseEvents = join(dataRoot, '..', '..', 'release-events.jsonl');
|
|
88
89
|
const raw = [
|
|
@@ -99,6 +100,9 @@ export async function collectProjectGraphSources(args) {
|
|
|
99
100
|
...((await fileExists(transitionLog))
|
|
100
101
|
? [{ id: 'transition-log', kind: 'transition-log', path: transitionLog }]
|
|
101
102
|
: []),
|
|
103
|
+
...((await fileExists(decisions))
|
|
104
|
+
? [{ id: 'decisions', kind: 'decisions', path: decisions }]
|
|
105
|
+
: []),
|
|
102
106
|
...((await fileExists(releaseEvents))
|
|
103
107
|
? [{ id: 'release-events', kind: 'release-events', path: releaseEvents }]
|
|
104
108
|
: []),
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ProjectGraphExtractionResult, ProjectGraphPolicy, ProjectGraphSource } from '../../../types/project-knowledge-graph.js';
|
|
2
|
+
export declare function extractDecisionStoreGraph(source: ProjectGraphSource, policy: ProjectGraphPolicy): ProjectGraphExtractionResult;
|
|
3
|
+
//# sourceMappingURL=decision-store-extractor.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { safeJsonParse } from '../../../storage/base-store.js';
|
|
2
|
+
import { hashText } from '../cache.js';
|
|
3
|
+
import { redactGraphText } from '../query.js';
|
|
4
|
+
function isDecision(value) {
|
|
5
|
+
return (value !== null &&
|
|
6
|
+
typeof value === 'object' &&
|
|
7
|
+
typeof value.id === 'string' &&
|
|
8
|
+
typeof value.what === 'string' &&
|
|
9
|
+
typeof value.why === 'string');
|
|
10
|
+
}
|
|
11
|
+
function decisionsFromSource(source) {
|
|
12
|
+
const parsed = safeJsonParse(source.content, { decisions: [] });
|
|
13
|
+
return Array.isArray(parsed.decisions) ? parsed.decisions.filter(isDecision) : [];
|
|
14
|
+
}
|
|
15
|
+
function node(args) {
|
|
16
|
+
return {
|
|
17
|
+
id: `decision:${args.decision.id}`,
|
|
18
|
+
type: 'decision',
|
|
19
|
+
label: redactGraphText(`${args.decision.what}: ${args.decision.why}`, args.policy),
|
|
20
|
+
source: 'decision-store-extractor',
|
|
21
|
+
evidence: [{ path: args.source.path, hash: args.source.hash }],
|
|
22
|
+
metadata: {
|
|
23
|
+
decisionId: args.decision.id,
|
|
24
|
+
specId: args.decision.specId ?? null,
|
|
25
|
+
category: args.decision.category,
|
|
26
|
+
status: args.decision.status,
|
|
27
|
+
decidedBy: args.decision.decidedBy,
|
|
28
|
+
},
|
|
29
|
+
updatedAt: args.decision.date,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function edge(decision, source) {
|
|
33
|
+
if (!decision.specId) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const decisionId = `decision:${decision.id}`;
|
|
37
|
+
return {
|
|
38
|
+
id: `edge:${hashText(`spec:${decision.specId}:decides:${decisionId}`)}`,
|
|
39
|
+
from: `spec:${decision.specId}`,
|
|
40
|
+
to: decisionId,
|
|
41
|
+
type: 'decides',
|
|
42
|
+
source: 'decision-store-extractor',
|
|
43
|
+
confidence: 'high',
|
|
44
|
+
classification: 'extracted',
|
|
45
|
+
evidence: { path: source.path, hash: source.hash },
|
|
46
|
+
updatedAt: decision.date,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function extractDecisionStoreGraph(source, policy) {
|
|
50
|
+
const decisions = decisionsFromSource(source);
|
|
51
|
+
const nodes = decisions.map((decision) => node({ decision, source, policy }));
|
|
52
|
+
const edges = decisions
|
|
53
|
+
.map((decision) => edge(decision, source))
|
|
54
|
+
.filter((item) => item !== null);
|
|
55
|
+
return { nodes, edges };
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=decision-store-extractor.js.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { clampLimit, loadFreshGraph, nextActionForFreshness, pagination, resolveProjectId, safeRelativeProjectPath, structuralPolicy, } from './helpers.js';
|
|
2
|
+
function aspects(input) {
|
|
3
|
+
const requested = input.aspects ?? ['all'];
|
|
4
|
+
if (requested.includes('all')) {
|
|
5
|
+
return new Set(['packages', 'hotspots', 'boundaries', 'releaseRisks', 'tests', 'decisions']);
|
|
6
|
+
}
|
|
7
|
+
return new Set(requested);
|
|
8
|
+
}
|
|
9
|
+
function packageName(path) {
|
|
10
|
+
const parts = path.split('/');
|
|
11
|
+
return parts.length > 1 ? parts.slice(0, 2).join('/') : (parts[0] ?? path);
|
|
12
|
+
}
|
|
13
|
+
function packageAspect(nodes, limit) {
|
|
14
|
+
const counts = new Map();
|
|
15
|
+
for (const node of nodes.filter((item) => item.type === 'file' || item.type === 'test')) {
|
|
16
|
+
counts.set(packageName(node.label), (counts.get(packageName(node.label)) ?? 0) + 1);
|
|
17
|
+
}
|
|
18
|
+
return [...counts.entries()]
|
|
19
|
+
.map(([name, fileCount]) => ({ name, fileCount }))
|
|
20
|
+
.sort((a, b) => b.fileCount - a.fileCount)
|
|
21
|
+
.slice(0, limit);
|
|
22
|
+
}
|
|
23
|
+
function hotspotAspect(nodes, edges, limit) {
|
|
24
|
+
const degree = new Map();
|
|
25
|
+
for (const edge of edges) {
|
|
26
|
+
degree.set(edge.from, (degree.get(edge.from) ?? 0) + 1);
|
|
27
|
+
degree.set(edge.to, (degree.get(edge.to) ?? 0) + 1);
|
|
28
|
+
}
|
|
29
|
+
return nodes
|
|
30
|
+
.map((node) => ({
|
|
31
|
+
id: node.id,
|
|
32
|
+
label: node.label,
|
|
33
|
+
type: node.type,
|
|
34
|
+
degree: degree.get(node.id) ?? 0,
|
|
35
|
+
}))
|
|
36
|
+
.filter((node) => node.degree > 0)
|
|
37
|
+
.sort((a, b) => b.degree - a.degree)
|
|
38
|
+
.slice(0, limit);
|
|
39
|
+
}
|
|
40
|
+
function boundaryAspect(edges, limit) {
|
|
41
|
+
return edges
|
|
42
|
+
.filter((edge) => edge.type === 'references' || edge.type === 'implements' || edge.type === 'tests')
|
|
43
|
+
.map((edge) => ({ from: edge.from, to: edge.to, type: edge.type }))
|
|
44
|
+
.slice(0, limit);
|
|
45
|
+
}
|
|
46
|
+
function simpleNodes(nodes, types, limit) {
|
|
47
|
+
return nodes
|
|
48
|
+
.filter((node) => types.includes(node.type))
|
|
49
|
+
.map((node) => ({ id: node.id, label: node.label, type: node.type }))
|
|
50
|
+
.slice(0, limit);
|
|
51
|
+
}
|
|
52
|
+
function snapshotAspects(args) {
|
|
53
|
+
const result = {};
|
|
54
|
+
if (args.requested.has('packages')) {
|
|
55
|
+
result.packages = packageAspect(args.nodes, args.limit);
|
|
56
|
+
}
|
|
57
|
+
if (args.requested.has('hotspots')) {
|
|
58
|
+
result.hotspots = hotspotAspect(args.nodes, args.edges, args.limit);
|
|
59
|
+
}
|
|
60
|
+
if (args.requested.has('boundaries')) {
|
|
61
|
+
result.boundaries = boundaryAspect(args.edges, args.limit);
|
|
62
|
+
}
|
|
63
|
+
if (args.requested.has('releaseRisks')) {
|
|
64
|
+
result.releaseRisks = simpleNodes(args.nodes, ['risk', 'release_risk', 'architecture_risk'], args.limit).map((node) => ({ id: node.id, label: node.label, type: node.type ?? 'risk' }));
|
|
65
|
+
}
|
|
66
|
+
if (args.requested.has('tests')) {
|
|
67
|
+
result.tests = simpleNodes(args.nodes, ['test'], args.limit).map(({ id, label }) => ({
|
|
68
|
+
id,
|
|
69
|
+
label,
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
if (args.requested.has('decisions')) {
|
|
73
|
+
result.decisions = simpleNodes(args.nodes, ['decision', 'adr'], args.limit).map(({ id, label }) => ({ id, label }));
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
export async function getArchitectureSnapshot(input) {
|
|
78
|
+
const policy = await structuralPolicy();
|
|
79
|
+
const projectId = resolveProjectId(input);
|
|
80
|
+
const limit = clampLimit(input.limit, policy);
|
|
81
|
+
const { graph, freshness } = await loadFreshGraph({
|
|
82
|
+
projectId,
|
|
83
|
+
projectPath: input.projectPath,
|
|
84
|
+
policy,
|
|
85
|
+
});
|
|
86
|
+
const prefix = input.pathPrefix === undefined
|
|
87
|
+
? undefined
|
|
88
|
+
: safeRelativeProjectPath(input.projectPath, input.pathPrefix);
|
|
89
|
+
if (input.pathPrefix !== undefined && prefix === null) {
|
|
90
|
+
const page = pagination(0, limit, 0);
|
|
91
|
+
return {
|
|
92
|
+
freshness,
|
|
93
|
+
pagination: page,
|
|
94
|
+
...page,
|
|
95
|
+
nextAction: {
|
|
96
|
+
type: 'inspect_evidence',
|
|
97
|
+
message: 'Rejecting unsafe pathPrefix filter; pass a project-relative path inside the repo.',
|
|
98
|
+
},
|
|
99
|
+
aspects: {},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const page = pagination(graph?.nodes.length ?? 0, limit, 0);
|
|
103
|
+
if (graph === null) {
|
|
104
|
+
return {
|
|
105
|
+
freshness,
|
|
106
|
+
pagination: page,
|
|
107
|
+
...page,
|
|
108
|
+
nextAction: nextActionForFreshness(freshness, true),
|
|
109
|
+
aspects: {},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const scopedNodes = graph.nodes.filter((node) => {
|
|
113
|
+
if (prefix === undefined) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
return node.label === prefix || node.label.startsWith(`${prefix}/`);
|
|
117
|
+
});
|
|
118
|
+
const scopedIds = new Set(scopedNodes.map((node) => node.id));
|
|
119
|
+
const scopedEdges = graph.edges.filter((edge) => scopedIds.has(edge.from) && scopedIds.has(edge.to));
|
|
120
|
+
const requested = aspects(input);
|
|
121
|
+
const result = snapshotAspects({ nodes: scopedNodes, edges: scopedEdges, requested, limit });
|
|
122
|
+
const scopedPage = pagination(scopedNodes.length, limit, 0);
|
|
123
|
+
return {
|
|
124
|
+
graphVersion: graph.graphVersion,
|
|
125
|
+
freshness,
|
|
126
|
+
pagination: scopedPage,
|
|
127
|
+
...scopedPage,
|
|
128
|
+
nextAction: nextActionForFreshness(freshness, scopedNodes.length === 0),
|
|
129
|
+
aspects: result,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=architecture-snapshot.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ProjectGraphPolicy } from '../../types/project-knowledge-graph.js';
|
|
2
|
+
import type { StructuralMemoryArtifactPolicy } from '../../types/structural-memory.js';
|
|
3
|
+
export declare function createStructuralMemoryArtifactPolicy(projectId: string, policy: ProjectGraphPolicy): StructuralMemoryArtifactPolicy;
|
|
4
|
+
export declare function isStructuralMemoryArtifactPathAllowed(path: string): boolean;
|
|
5
|
+
//# sourceMappingURL=artifact-policy.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { projectGraphDir, projectGraphPath } from '../project-graph/cache.js';
|
|
2
|
+
export function createStructuralMemoryArtifactPolicy(projectId, policy) {
|
|
3
|
+
const artifactRoot = projectGraphDir(projectId, policy);
|
|
4
|
+
return {
|
|
5
|
+
artifactRoot,
|
|
6
|
+
graphPath: projectGraphPath(projectId, policy),
|
|
7
|
+
projectDataDirectory: artifactRoot.split('/project-graph')[0] ?? artifactRoot,
|
|
8
|
+
forbiddenPathFragment: 'planu/specs',
|
|
9
|
+
specDirectoryPolicy: 'spec.md-only',
|
|
10
|
+
evidenceAndGraphArtifactsRemainOutsideSpecs: true,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function isStructuralMemoryArtifactPathAllowed(path) {
|
|
14
|
+
const normalized = path.replace(/\\/g, '/');
|
|
15
|
+
return !normalized.includes('/planu/specs/') && !normalized.endsWith('/planu/specs');
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=artifact-policy.js.map
|