@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.
Files changed (59) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/config/license-plans.json +5 -0
  3. package/dist/config/project-knowledge-graph.json +70 -2
  4. package/dist/config/registries/hosts/codex.json +3 -4
  5. package/dist/engine/evidence-index/index-builder.js +38 -12
  6. package/dist/engine/project-graph/builder.js +9 -5
  7. package/dist/engine/project-graph/cache.js +4 -0
  8. package/dist/engine/project-graph/extractors/decision-store-extractor.d.ts +3 -0
  9. package/dist/engine/project-graph/extractors/decision-store-extractor.js +57 -0
  10. package/dist/engine/structural-memory/architecture-snapshot.d.ts +3 -0
  11. package/dist/engine/structural-memory/architecture-snapshot.js +132 -0
  12. package/dist/engine/structural-memory/artifact-policy.d.ts +5 -0
  13. package/dist/engine/structural-memory/artifact-policy.js +17 -0
  14. package/dist/engine/structural-memory/change-impact.d.ts +3 -0
  15. package/dist/engine/structural-memory/change-impact.js +159 -0
  16. package/dist/engine/structural-memory/contracts.d.ts +2 -0
  17. package/dist/engine/structural-memory/contracts.js +2 -0
  18. package/dist/engine/structural-memory/helpers.d.ts +28 -0
  19. package/dist/engine/structural-memory/helpers.js +122 -0
  20. package/dist/engine/structural-memory/index.d.ts +7 -0
  21. package/dist/engine/structural-memory/index.js +6 -0
  22. package/dist/engine/structural-memory/query.d.ts +3 -0
  23. package/dist/engine/structural-memory/query.js +67 -0
  24. package/dist/engine/structural-memory/status.d.ts +3 -0
  25. package/dist/engine/structural-memory/status.js +30 -0
  26. package/dist/storage/feedback-remote.d.ts +5 -1
  27. package/dist/storage/feedback-remote.js +75 -16
  28. package/dist/storage/feedback-store.d.ts +3 -1
  29. package/dist/storage/feedback-store.js +107 -10
  30. package/dist/tools/feedback-handler.d.ts +3 -1
  31. package/dist/tools/feedback-handler.js +47 -1
  32. package/dist/tools/register-sdd-tools.d.ts +1 -1
  33. package/dist/tools/register-sdd-tools.js +5 -0
  34. package/dist/tools/status-handler.js +11 -11
  35. package/dist/tools/tool-registry/core-tools.js +97 -0
  36. package/dist/tools/tool-registry.js +3 -0
  37. package/dist/tools/validate-assurance.d.ts +6 -0
  38. package/dist/tools/validate-assurance.js +55 -0
  39. package/dist/tools/validate-graph-coverage.d.ts +8 -0
  40. package/dist/tools/validate-graph-coverage.js +30 -0
  41. package/dist/tools/validate-helpers.d.ts +9 -0
  42. package/dist/tools/validate-helpers.js +47 -0
  43. package/dist/tools/validate-lint.d.ts +3 -0
  44. package/dist/tools/validate-lint.js +67 -0
  45. package/dist/tools/validate-minimality.d.ts +16 -0
  46. package/dist/tools/validate-minimality.js +22 -0
  47. package/dist/tools/validate-runtime.d.ts +14 -0
  48. package/dist/tools/validate-runtime.js +85 -0
  49. package/dist/tools/validate.js +5 -222
  50. package/dist/types/evidence-index.d.ts +1 -0
  51. package/dist/types/feedback.d.ts +37 -0
  52. package/dist/types/index.d.ts +2 -0
  53. package/dist/types/index.js +2 -0
  54. package/dist/types/project-knowledge-graph.d.ts +10 -0
  55. package/dist/types/structural-memory.d.ts +138 -0
  56. package/dist/types/structural-memory.js +2 -0
  57. package/package.json +20 -20
  58. package/planu-native.json +1 -1
  59. 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
- return record(kind, value, exists ? 'valid' : 'stale', source, exists ? undefined : 'file missing');
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: 'empty evidence path' };
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: 'absolute evidence paths are not allowed' };
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: 'path traversal is not allowed' };
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 nodeIds = new Set(nodes.map((node) => node.id));
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 edges = uniqueEdges(validated.flatMap((result) => result.edges));
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,3 @@
1
+ import type { ArchitectureSnapshotInput, ArchitectureSnapshotResult } from './contracts.js';
2
+ export declare function getArchitectureSnapshot(input: ArchitectureSnapshotInput): Promise<ArchitectureSnapshotResult>;
3
+ //# sourceMappingURL=architecture-snapshot.d.ts.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
@@ -0,0 +1,3 @@
1
+ import type { StructuralChangeImpactInput, StructuralChangeImpactResult } from './contracts.js';
2
+ export declare function detectStructuralChangeImpact(input: StructuralChangeImpactInput): Promise<StructuralChangeImpactResult>;
3
+ //# sourceMappingURL=change-impact.d.ts.map