@planu/cli 4.10.4 → 4.10.6
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 +17 -1
- package/dist/config/version.js +1 -1
- package/dist/config/version.ts +1 -1
- package/dist/engine/compact/tool-list-compactor.d.ts +1 -1
- package/dist/engine/evidence-gates/lifecycle-gate.js +17 -1
- package/dist/engine/evidence-index/done-drift.js +4 -1
- package/dist/engine/local-first/tool-classification.d.ts +6 -0
- package/dist/engine/local-first/tool-classification.js +119 -0
- package/dist/engine/project-graph/extractors/decision-store-extractor.js +1 -1
- package/dist/engine/project-graph/extractors/git-extractor.js +1 -1
- package/dist/engine/project-graph/extractors/handoff-extractor.js +1 -1
- package/dist/engine/project-graph/extractors/spec-extractor.js +1 -1
- package/dist/engine/project-graph/extractors/validation-extractor.js +1 -1
- package/dist/engine/project-graph/index.d.ts +2 -1
- package/dist/engine/project-graph/index.js +2 -1
- package/dist/engine/project-graph/query.d.ts +1 -2
- package/dist/engine/project-graph/query.js +1 -10
- package/dist/engine/project-graph/redaction.d.ts +4 -0
- package/dist/engine/project-graph/redaction.js +11 -0
- package/dist/engine/qa-gate.js +13 -1
- package/dist/engine/scope-boundaries/scope-validator.js +14 -2
- package/dist/engine/validator/validation-report-writer.js +39 -8
- package/dist/tools/design-schema-sql/migrations.js +8 -3
- package/dist/tools/design-schema-sql/tables.js +19 -3
- package/dist/tools/list-specs.js +15 -59
- package/dist/tools/output-compressor.d.ts +14 -1
- package/dist/tools/output-compressor.js +121 -2
- package/dist/tools/package-handoff.js +136 -12
- package/dist/tools/status-handler.js +6 -1
- package/dist/tools/token-recording.d.ts +2 -1
- package/dist/tools/token-recording.js +21 -2
- package/dist/tools/update-status/dod-gates.d.ts +2 -2
- package/dist/tools/update-status/dod-gates.js +8 -28
- package/dist/tools/update-status/index.js +7 -1
- package/dist/tools/update-status/qa-gate.d.ts +5 -0
- package/dist/tools/update-status/qa-gate.js +50 -0
- package/dist/tools/update-status/response-builder.js +96 -2
- package/dist/tools/update-status-convention-gate.js +22 -6
- package/dist/tools/validate-lint.js +1 -1
- package/dist/tools/validate.js +147 -19
- package/dist/transports/transport-factory.js +13 -0
- package/dist/types/qa-gate.d.ts +3 -0
- package/dist/types/tool-classification.d.ts +8 -0
- package/dist/types/tool-classification.js +2 -0
- package/package.json +11 -18
- package/planu-native.json +8 -29
- package/planu-plugin.json +7 -35
- package/dist/engine/context-artifacts/index.d.ts +0 -2
- package/dist/engine/context-artifacts/index.js +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
## [4.10.6] - 2026-07-07
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
- fix(SPEC-1111): stabilize update_status done gates and evidence state transitions
|
|
5
|
+
- fix(SPEC-1112): preserve validation-report lint evidence during done gate
|
|
6
|
+
|
|
7
|
+
### Improvements
|
|
8
|
+
- refactor(SPEC-1110): reduce MCP token payloads with local-first tool classification
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## [4.10.5] - 2026-07-07
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
- fix(SPEC-1109): resolve audit backlog
|
|
15
|
+
|
|
16
|
+
|
|
1
17
|
## [4.10.4] - 2026-07-06
|
|
2
18
|
|
|
3
19
|
### Bug Fixes
|
|
@@ -4183,4 +4199,4 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning:
|
|
|
4183
4199
|
- Mermaid diagram generation (architecture, sequence, state machine, ER, data flow)
|
|
4184
4200
|
- Multi-language i18n (EN/ES/PT) for generated specs
|
|
4185
4201
|
- Clean Architecture (hexagonal) — engine, tools, storage, types layers
|
|
4186
|
-
- 10,857 tests with ≥95% coverage
|
|
4202
|
+
- 10,857 tests with ≥95% coverage
|
package/dist/config/version.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// version.ts — reads from package.json at runtime; updated
|
|
1
|
+
// version.ts — reads from package.json at runtime; updated by the local release flow
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
package/dist/config/version.ts
CHANGED
|
@@ -3,5 +3,5 @@ export declare function compactToolList(tools: unknown[]): Record<string, unknow
|
|
|
3
3
|
/** Returns true when an outgoing JSON-RPC message is a tools/list result. */
|
|
4
4
|
export declare function isToolsListResponse(message: unknown): boolean;
|
|
5
5
|
/** Compact a tools/list JSON-RPC response message. Returns a new object. */
|
|
6
|
-
export declare function compactToolsListMessage
|
|
6
|
+
export declare function compactToolsListMessage<T extends Record<string, unknown>>(message: T): T;
|
|
7
7
|
//# sourceMappingURL=tool-list-compactor.d.ts.map
|
|
@@ -114,9 +114,25 @@ export function checkDoneEvidenceGate(spec, criteria, artifacts) {
|
|
|
114
114
|
!hasText(row.validationEvidence) ||
|
|
115
115
|
!hasText(row.reviewerEvidence));
|
|
116
116
|
if (incompleteRows.length > 0) {
|
|
117
|
+
const rowDetails = incompleteRows.map((row) => {
|
|
118
|
+
const missing = [];
|
|
119
|
+
if (!rowHasEvidence(row)) {
|
|
120
|
+
missing.push('scenario/testEvidence/contractEvidence/manualEvidence');
|
|
121
|
+
}
|
|
122
|
+
if (row.changedFiles.length === 0) {
|
|
123
|
+
missing.push('changedFiles');
|
|
124
|
+
}
|
|
125
|
+
if (!hasText(row.validationEvidence)) {
|
|
126
|
+
missing.push('validationEvidence');
|
|
127
|
+
}
|
|
128
|
+
if (!hasText(row.reviewerEvidence)) {
|
|
129
|
+
missing.push('reviewerEvidence');
|
|
130
|
+
}
|
|
131
|
+
return `${row.acceptanceCriterion}: missing ${missing.join(', ')}`;
|
|
132
|
+
});
|
|
117
133
|
issues.push({
|
|
118
134
|
code: 'traceability_incomplete_rows',
|
|
119
|
-
message:
|
|
135
|
+
message: `Traceability rows must include evidence, changed files, validation evidence, and reviewer evidence. Incomplete rows: ${rowDetails.join('; ')}`,
|
|
120
136
|
});
|
|
121
137
|
}
|
|
122
138
|
}
|
|
@@ -33,9 +33,12 @@ export function checkDoneDriftContract(args) {
|
|
|
33
33
|
.map((record) => `${entry.criterion}: ${record.value}`);
|
|
34
34
|
});
|
|
35
35
|
if (unapproved.length > 0) {
|
|
36
|
+
const groundedHint = args.groundedTechnicalPaths.length > 0
|
|
37
|
+
? ` Grounded technical paths: ${args.groundedTechnicalPaths.join(', ')}.`
|
|
38
|
+
: ' No grounded technical paths were found in the approved spec.';
|
|
36
39
|
issues.push({
|
|
37
40
|
code: 'done_drift_unapproved_scope',
|
|
38
|
-
message: `Done drift check found changed files outside grounded spec scope: ${unapproved.join('; ')}
|
|
41
|
+
message: `Done drift check found changed files outside grounded spec scope: ${unapproved.join('; ')}.${groundedHint} Add the file to approved technical references or include reviewerEvidence with an approved scope amendment.`,
|
|
39
42
|
});
|
|
40
43
|
}
|
|
41
44
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ToolClassification } from '../../types/tool-classification.js';
|
|
2
|
+
export declare function getToolClassification(toolName: string): ToolClassification;
|
|
3
|
+
export declare function isLocalFirstTool(toolName: string): boolean;
|
|
4
|
+
export declare function requiresLlmByDefault(toolName: string): boolean;
|
|
5
|
+
export declare function listToolClassifications(): ToolClassification[];
|
|
6
|
+
//# sourceMappingURL=tool-classification.d.ts.map
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const TOOL_CLASSIFICATIONS = new Map([
|
|
2
|
+
[
|
|
3
|
+
'create_spec',
|
|
4
|
+
{
|
|
5
|
+
toolName: 'create_spec',
|
|
6
|
+
executionClass: 'llm-required',
|
|
7
|
+
tokenPolicy: 'llm-product',
|
|
8
|
+
reason: 'Generates the primary spec artifact and needs language synthesis.',
|
|
9
|
+
},
|
|
10
|
+
],
|
|
11
|
+
[
|
|
12
|
+
'validate',
|
|
13
|
+
{
|
|
14
|
+
toolName: 'validate',
|
|
15
|
+
executionClass: 'local',
|
|
16
|
+
tokenPolicy: 'summarize-first',
|
|
17
|
+
reason: 'Compares repository state and spec artifacts with deterministic validators.',
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
[
|
|
21
|
+
'audit',
|
|
22
|
+
{
|
|
23
|
+
toolName: 'audit',
|
|
24
|
+
executionClass: 'local',
|
|
25
|
+
tokenPolicy: 'summarize-first',
|
|
26
|
+
reason: 'Runs deterministic checks and returns issue summaries.',
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
[
|
|
30
|
+
'package_handoff',
|
|
31
|
+
{
|
|
32
|
+
toolName: 'package_handoff',
|
|
33
|
+
executionClass: 'local',
|
|
34
|
+
tokenPolicy: 'summarize-first',
|
|
35
|
+
reason: 'Packages existing spec and project context into an artifact reference.',
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
[
|
|
39
|
+
'list_specs',
|
|
40
|
+
{
|
|
41
|
+
toolName: 'list_specs',
|
|
42
|
+
executionClass: 'local',
|
|
43
|
+
tokenPolicy: 'summarize-first',
|
|
44
|
+
reason: 'Lists indexed spec metadata and must stay read-only.',
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
[
|
|
48
|
+
'summarize_spec',
|
|
49
|
+
{
|
|
50
|
+
toolName: 'summarize_spec',
|
|
51
|
+
executionClass: 'local',
|
|
52
|
+
tokenPolicy: 'summarize-first',
|
|
53
|
+
reason: 'Extracts bounded decision fields from an existing spec.',
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
[
|
|
57
|
+
'generate_tests',
|
|
58
|
+
{
|
|
59
|
+
toolName: 'generate_tests',
|
|
60
|
+
executionClass: 'hybrid',
|
|
61
|
+
tokenPolicy: 'summarize-first',
|
|
62
|
+
reason: 'Can generate deterministic scaffolds locally; novel test design may need LLM input.',
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
[
|
|
66
|
+
'recommend_model',
|
|
67
|
+
{
|
|
68
|
+
toolName: 'recommend_model',
|
|
69
|
+
executionClass: 'local',
|
|
70
|
+
tokenPolicy: 'no-llm',
|
|
71
|
+
reason: 'Uses local heuristics and project metadata for routing advice.',
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
[
|
|
75
|
+
'multi_agent_review',
|
|
76
|
+
{
|
|
77
|
+
toolName: 'multi_agent_review',
|
|
78
|
+
executionClass: 'hybrid',
|
|
79
|
+
tokenPolicy: 'summarize-first',
|
|
80
|
+
reason: 'Local orchestration and evidence collection precede any LLM review synthesis.',
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
[
|
|
84
|
+
'audit_specs_drift',
|
|
85
|
+
{
|
|
86
|
+
toolName: 'audit_specs_drift',
|
|
87
|
+
executionClass: 'local',
|
|
88
|
+
tokenPolicy: 'summarize-first',
|
|
89
|
+
reason: 'Diffs spec and implementation artifacts locally.',
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
[
|
|
93
|
+
'orchestrate_runtime',
|
|
94
|
+
{
|
|
95
|
+
toolName: 'orchestrate_runtime',
|
|
96
|
+
executionClass: 'local',
|
|
97
|
+
tokenPolicy: 'no-llm',
|
|
98
|
+
reason: 'Selects runtime behavior from local state and explicit policy.',
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
]);
|
|
102
|
+
export function getToolClassification(toolName) {
|
|
103
|
+
return (TOOL_CLASSIFICATIONS.get(toolName) ?? {
|
|
104
|
+
toolName,
|
|
105
|
+
executionClass: 'hybrid',
|
|
106
|
+
tokenPolicy: 'summarize-first',
|
|
107
|
+
reason: 'Unregistered tools default to local preprocessing with bounded summaries.',
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
export function isLocalFirstTool(toolName) {
|
|
111
|
+
return getToolClassification(toolName).executionClass !== 'llm-required';
|
|
112
|
+
}
|
|
113
|
+
export function requiresLlmByDefault(toolName) {
|
|
114
|
+
return getToolClassification(toolName).executionClass === 'llm-required';
|
|
115
|
+
}
|
|
116
|
+
export function listToolClassifications() {
|
|
117
|
+
return [...TOOL_CLASSIFICATIONS.values()].sort((a, b) => a.toolName.localeCompare(b.toolName));
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=tool-classification.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { safeJsonParse } from '../../../storage/base-store.js';
|
|
2
2
|
import { hashText } from '../cache.js';
|
|
3
|
-
import { redactGraphText } from '../
|
|
3
|
+
import { redactGraphText } from '../redaction.js';
|
|
4
4
|
function isDecision(value) {
|
|
5
5
|
return (value !== null &&
|
|
6
6
|
typeof value === 'object' &&
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { promisify } from 'node:util';
|
|
2
2
|
import { hashText } from '../cache.js';
|
|
3
|
-
import { redactGraphText } from '../
|
|
3
|
+
import { redactGraphText } from '../redaction.js';
|
|
4
4
|
export async function extractGitGraph(args) {
|
|
5
5
|
try {
|
|
6
6
|
const childProcess = await import('node:child_process');
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { buildProjectKnowledgeGraph } from './builder.js';
|
|
2
|
-
export { queryProjectGraphSlice, formatProjectGraphContext, getProjectGraphFreshnessHint,
|
|
2
|
+
export { queryProjectGraphSlice, formatProjectGraphContext, getProjectGraphFreshnessHint, } from './query.js';
|
|
3
|
+
export { redactGraphText } from './redaction.js';
|
|
3
4
|
export { getProjectGraphFreshness, loadProjectGraphPolicy, projectGraphPath, projectGraphCachePath, } from './cache.js';
|
|
4
5
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { buildProjectKnowledgeGraph } from './builder.js';
|
|
2
|
-
export { queryProjectGraphSlice, formatProjectGraphContext, getProjectGraphFreshnessHint,
|
|
2
|
+
export { queryProjectGraphSlice, formatProjectGraphContext, getProjectGraphFreshnessHint, } from './query.js';
|
|
3
|
+
export { redactGraphText } from './redaction.js';
|
|
3
4
|
export { getProjectGraphFreshness, loadProjectGraphPolicy, projectGraphPath, projectGraphCachePath, } from './cache.js';
|
|
4
5
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function redactGraphText(text: string, policy: ProjectGraphPolicy, maxChars?: number): string;
|
|
1
|
+
import type { ProjectGraphQueryInput, ProjectGraphSlice } from '../../types/project-knowledge-graph.js';
|
|
3
2
|
export declare function queryProjectGraphSlice(input: ProjectGraphQueryInput): Promise<ProjectGraphSlice | null>;
|
|
4
3
|
export declare function formatProjectGraphContext(slice: ProjectGraphSlice): Promise<string>;
|
|
5
4
|
export declare function getProjectGraphFreshnessHint(args: {
|
|
@@ -1,16 +1,7 @@
|
|
|
1
1
|
import { hashProjectPath } from '../../storage/base-store.js';
|
|
2
2
|
import { buildProjectKnowledgeGraph } from './builder.js';
|
|
3
3
|
import { getProjectGraphFreshness, loadProjectGraphPolicy, readProjectGraph } from './cache.js';
|
|
4
|
-
|
|
5
|
-
return text.replace(/</g, '<').replace(/>/g, '>');
|
|
6
|
-
}
|
|
7
|
-
export function redactGraphText(text, policy, maxChars = policy.redaction.maxSnippetChars) {
|
|
8
|
-
const truncated = text.slice(0, maxChars);
|
|
9
|
-
return policy.redaction.redactPatterns.reduce((current, pattern) => {
|
|
10
|
-
const re = new RegExp(pattern, 'gi');
|
|
11
|
-
return current.replace(re, '[redacted]');
|
|
12
|
-
}, truncated);
|
|
13
|
-
}
|
|
4
|
+
import { escapeInert, redactGraphText } from './redaction.js';
|
|
14
5
|
function sanitizeLabel(label, policy) {
|
|
15
6
|
return escapeInert(redactGraphText(label, policy));
|
|
16
7
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ProjectGraphPolicy } from '../../types/project-knowledge-graph.js';
|
|
2
|
+
export declare function escapeInert(text: string): string;
|
|
3
|
+
export declare function redactGraphText(text: string, policy: ProjectGraphPolicy, maxChars?: number): string;
|
|
4
|
+
//# sourceMappingURL=redaction.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function escapeInert(text) {
|
|
2
|
+
return text.replace(/</g, '<').replace(/>/g, '>');
|
|
3
|
+
}
|
|
4
|
+
export function redactGraphText(text, policy, maxChars = policy.redaction.maxSnippetChars) {
|
|
5
|
+
const truncated = text.slice(0, maxChars);
|
|
6
|
+
return policy.redaction.redactPatterns.reduce((current, pattern) => {
|
|
7
|
+
const re = new RegExp(pattern, 'gi');
|
|
8
|
+
return current.replace(re, '[redacted]');
|
|
9
|
+
}, truncated);
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=redaction.js.map
|
package/dist/engine/qa-gate.js
CHANGED
|
@@ -20,7 +20,19 @@ function runCheck(name, command, args, cwd) {
|
|
|
20
20
|
const durationMs = Date.now() - start;
|
|
21
21
|
const passed = result.status === 0 && !result.error;
|
|
22
22
|
const output = [result.stdout, result.stderr].join('\n').trim().slice(0, 2000);
|
|
23
|
-
|
|
23
|
+
const errorCode = result.error && 'code' in result.error
|
|
24
|
+
? String(result.error.code)
|
|
25
|
+
: undefined;
|
|
26
|
+
const timedOut = errorCode === 'ETIMEDOUT' || result.signal === 'SIGTERM';
|
|
27
|
+
return {
|
|
28
|
+
name,
|
|
29
|
+
passed,
|
|
30
|
+
output,
|
|
31
|
+
durationMs,
|
|
32
|
+
command: [command, ...args].join(' '),
|
|
33
|
+
timedOut,
|
|
34
|
+
errorMessage: result.error?.message,
|
|
35
|
+
};
|
|
24
36
|
}
|
|
25
37
|
export function runQaGate(spec, projectPath) {
|
|
26
38
|
const coverageThreshold = extractCoverageThreshold(spec);
|
|
@@ -11,11 +11,23 @@ function normalize(text) {
|
|
|
11
11
|
/**
|
|
12
12
|
* Extract keywords from an out-of-scope item phrase (>3 chars).
|
|
13
13
|
*/
|
|
14
|
-
const STOP_WORDS = new Set(['the', 'and', 'for', 'with', 'that', 'this', 'from', 'will']);
|
|
14
|
+
const STOP_WORDS = new Set(['the', 'and', 'for', 'with', 'that', 'this', 'from', 'will', 'every']);
|
|
15
|
+
const GENERIC_SCOPE_TOKENS = new Set([
|
|
16
|
+
'code',
|
|
17
|
+
'file',
|
|
18
|
+
'files',
|
|
19
|
+
'module',
|
|
20
|
+
'modules',
|
|
21
|
+
'mcp',
|
|
22
|
+
'response',
|
|
23
|
+
'responses',
|
|
24
|
+
'tool',
|
|
25
|
+
'tools',
|
|
26
|
+
]);
|
|
15
27
|
function keywords(phrase) {
|
|
16
28
|
return normalize(phrase)
|
|
17
29
|
.split(' ')
|
|
18
|
-
.filter((w) => w.length >
|
|
30
|
+
.filter((w) => w.length > 2 && !STOP_WORDS.has(w) && !GENERIC_SCOPE_TOKENS.has(w));
|
|
19
31
|
}
|
|
20
32
|
/**
|
|
21
33
|
* Determine whether a file path matches an out-of-scope item.
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import { appendArtifact } from '../handoff-artifacts/io.js';
|
|
1
|
+
import { appendArtifact, readArtifact } from '../handoff-artifacts/io.js';
|
|
2
2
|
import { runSpecCompliance, } from './spec-compliance-runner.js';
|
|
3
3
|
export async function writeImplementationReviewReport(input) {
|
|
4
4
|
const specCompliance = await runSpecCompliance(input.spec, input.projectPath);
|
|
5
5
|
const gates = buildGates({
|
|
6
6
|
score: input.score,
|
|
7
|
-
lintPassed: input.lintPassed
|
|
7
|
+
lintPassed: input.lintPassed,
|
|
8
8
|
conventionRegression: input.conventionRegression ?? false,
|
|
9
9
|
specCompliance,
|
|
10
10
|
minimalityReport: input.minimalityReport,
|
|
11
11
|
});
|
|
12
|
-
const
|
|
12
|
+
const mergedGates = await preserveUnobservedGates(input, gates);
|
|
13
|
+
const passed = mergedGates.every((gate) => gate.passed);
|
|
13
14
|
const reviewer = {
|
|
14
15
|
kind: 'implementation-review-agent',
|
|
15
16
|
agent: 'planu-implementation-reviewer',
|
|
@@ -20,7 +21,7 @@ export async function writeImplementationReviewReport(input) {
|
|
|
20
21
|
schema_version: '1.0.0',
|
|
21
22
|
specId: input.specId,
|
|
22
23
|
passed,
|
|
23
|
-
gates,
|
|
24
|
+
gates: mergedGates,
|
|
24
25
|
reviewer,
|
|
25
26
|
specCompliance: reportCompliance,
|
|
26
27
|
minimalityReport: input.minimalityReport,
|
|
@@ -40,7 +41,7 @@ export async function writeImplementationReviewReport(input) {
|
|
|
40
41
|
sha: artifact.sha,
|
|
41
42
|
reviewer,
|
|
42
43
|
passed,
|
|
43
|
-
gates,
|
|
44
|
+
gates: mergedGates,
|
|
44
45
|
specCompliance: reportCompliance,
|
|
45
46
|
};
|
|
46
47
|
}
|
|
@@ -50,11 +51,37 @@ export async function writeImplementationReviewReport(input) {
|
|
|
50
51
|
error: err instanceof Error ? err.message : String(err),
|
|
51
52
|
reviewer,
|
|
52
53
|
passed,
|
|
53
|
-
gates,
|
|
54
|
+
gates: mergedGates,
|
|
54
55
|
specCompliance: reportCompliance,
|
|
55
56
|
};
|
|
56
57
|
}
|
|
57
58
|
}
|
|
59
|
+
async function preserveUnobservedGates(input, nextGates) {
|
|
60
|
+
const currentGateNames = new Set(nextGates.map((gate) => gate.name));
|
|
61
|
+
const explicitlyObserved = new Set([
|
|
62
|
+
...(input.lintPassed !== undefined ? ['lint'] : []),
|
|
63
|
+
...(input.conventionRegression !== undefined ? ['convention-regression'] : []),
|
|
64
|
+
...(input.minimalityReport !== undefined ? ['minimal-implementation'] : []),
|
|
65
|
+
]);
|
|
66
|
+
const existing = await readArtifact({
|
|
67
|
+
projectId: input.projectId,
|
|
68
|
+
specId: input.specId,
|
|
69
|
+
kind: 'validation-report',
|
|
70
|
+
}).catch(() => null);
|
|
71
|
+
if (!existing?.ok) {
|
|
72
|
+
return nextGates;
|
|
73
|
+
}
|
|
74
|
+
const preserved = existing.payload.gates.filter((gate) => currentGateNames.has(gate.name) &&
|
|
75
|
+
!explicitlyObserved.has(gate.name) &&
|
|
76
|
+
(gate.name === 'lint' ||
|
|
77
|
+
gate.name === 'convention-regression' ||
|
|
78
|
+
gate.name === 'minimal-implementation'));
|
|
79
|
+
if (preserved.length === 0) {
|
|
80
|
+
return nextGates;
|
|
81
|
+
}
|
|
82
|
+
const preservedByName = new Map(preserved.map((gate) => [gate.name, gate]));
|
|
83
|
+
return nextGates.map((gate) => preservedByName.get(gate.name) ?? gate);
|
|
84
|
+
}
|
|
58
85
|
function buildGates(args) {
|
|
59
86
|
return [
|
|
60
87
|
{
|
|
@@ -66,8 +93,12 @@ function buildGates(args) {
|
|
|
66
93
|
},
|
|
67
94
|
{
|
|
68
95
|
name: 'lint',
|
|
69
|
-
passed: args.lintPassed,
|
|
70
|
-
reason: args.lintPassed
|
|
96
|
+
passed: args.lintPassed === true,
|
|
97
|
+
reason: args.lintPassed === true
|
|
98
|
+
? undefined
|
|
99
|
+
: args.lintPassed === false
|
|
100
|
+
? 'Configured lint command failed.'
|
|
101
|
+
: 'Lint result was not provided; report cannot claim lint passed.',
|
|
71
102
|
},
|
|
72
103
|
{
|
|
73
104
|
name: 'convention-regression',
|
|
@@ -63,11 +63,16 @@ export function generateMigrations(tables, indexes, rlsPolicies, engine) {
|
|
|
63
63
|
}
|
|
64
64
|
for (const policy of rlsPolicies) {
|
|
65
65
|
const rolesClause = policy.roles?.length ? ` TO ${policy.roles.join(', ')}` : '';
|
|
66
|
-
const usingClause = `USING (${policy.using})`;
|
|
67
|
-
const withCheckClause = policy.withCheck
|
|
66
|
+
const usingClause = policy.operation === 'INSERT' ? '' : ` USING (${policy.using})`;
|
|
67
|
+
const withCheckClause = policy.withCheck || policy.operation === 'INSERT'
|
|
68
|
+
? ` WITH CHECK (${policy.withCheck ?? policy.using})`
|
|
69
|
+
: '';
|
|
70
|
+
const policyClauses = [rolesClause, usingClause, withCheckClause]
|
|
71
|
+
.filter((clause) => clause.length > 0)
|
|
72
|
+
.join('');
|
|
68
73
|
migrations.push({
|
|
69
74
|
order: order++,
|
|
70
|
-
sql: `CREATE POLICY ${policy.name} ON ${policy.table} FOR ${policy.operation}${
|
|
75
|
+
sql: `CREATE POLICY ${policy.name} ON ${policy.table} FOR ${policy.operation}${policyClauses};`,
|
|
71
76
|
reversible: true,
|
|
72
77
|
reverseSql: `DROP POLICY IF EXISTS ${policy.name} ON ${policy.table};`,
|
|
73
78
|
dataLoss: false,
|
|
@@ -210,12 +210,28 @@ export function generateRLSPolicies(tables) {
|
|
|
210
210
|
}
|
|
211
211
|
else {
|
|
212
212
|
policies.push({
|
|
213
|
-
name: `${table.name}
|
|
213
|
+
name: `${table.name}_insert_authenticated`,
|
|
214
214
|
table: table.name,
|
|
215
|
-
operation: '
|
|
215
|
+
operation: 'INSERT',
|
|
216
|
+
roles: ['authenticated'],
|
|
217
|
+
using: 'true',
|
|
218
|
+
withCheck: 'true',
|
|
219
|
+
justification: 'Authenticated users can insert records',
|
|
220
|
+
}, {
|
|
221
|
+
name: `${table.name}_update_authenticated`,
|
|
222
|
+
table: table.name,
|
|
223
|
+
operation: 'UPDATE',
|
|
224
|
+
roles: ['authenticated'],
|
|
225
|
+
using: 'true',
|
|
226
|
+
withCheck: 'true',
|
|
227
|
+
justification: 'Authenticated users can update records',
|
|
228
|
+
}, {
|
|
229
|
+
name: `${table.name}_delete_authenticated`,
|
|
230
|
+
table: table.name,
|
|
231
|
+
operation: 'DELETE',
|
|
216
232
|
roles: ['authenticated'],
|
|
217
233
|
using: 'true',
|
|
218
|
-
justification: '
|
|
234
|
+
justification: 'Authenticated users can delete records',
|
|
219
235
|
});
|
|
220
236
|
}
|
|
221
237
|
}
|
package/dist/tools/list-specs.js
CHANGED
|
@@ -4,16 +4,15 @@ import { buildListSpecsSummary } from '../engine/human-summary.js';
|
|
|
4
4
|
import { dirname } from 'node:path';
|
|
5
5
|
import { checkBundledVersionGap } from '../engine/version-detector/bundled-version-checker.js';
|
|
6
6
|
import { readLastModifiedTimestamp, formatRelativeDate } from '../engine/spec-changelog/index.js';
|
|
7
|
-
import {
|
|
7
|
+
import { scanForAmbiguousCriteria } from '../engine/spec-migrator.js';
|
|
8
8
|
import { detectDrift, formatDriftMessage } from '../engine/spec-migrator/drift-detector.js';
|
|
9
9
|
import { AutopilotSummaryCollector } from '../engine/autopilot/summary-collector.js';
|
|
10
|
-
import { withAudit } from '../engine/autopilot/audit-logger.js';
|
|
11
10
|
import { trackCost } from '../engine/cost-tracking/operation-tracker.js';
|
|
12
|
-
import { refreshProjectStatus } from '../engine/autopilot/state-updater.js';
|
|
13
11
|
import { resolveProjectIdOrAutoDetect } from './resolve-project-id.js';
|
|
14
12
|
import { isNativeActive, fastScanSpecs } from '../engine/core-bridge.js';
|
|
15
13
|
/** Track which projects have been auto-discovered this session (once per project). */
|
|
16
14
|
const discoveredProjects = new Set();
|
|
15
|
+
const MAX_STRUCTURED_SPECS = 50;
|
|
17
16
|
/** Build markdown table rows for summary mode */
|
|
18
17
|
function buildSummaryTable(specs) {
|
|
19
18
|
const header = '| ID | Title | Status | Type | Tags |';
|
|
@@ -21,48 +20,11 @@ function buildSummaryTable(specs) {
|
|
|
21
20
|
const rows = specs.map((s) => `| ${s.id} | ${s.title} | ${s.status} | ${s.type} | ${s.tags.join(', ')} |`);
|
|
22
21
|
return [header, divider, ...rows].join('\n');
|
|
23
22
|
}
|
|
24
|
-
async function
|
|
23
|
+
async function collectReadOnlyProjectSignals(projectId, projectPath, collector) {
|
|
25
24
|
if (discoveredProjects.has(projectId)) {
|
|
26
25
|
return;
|
|
27
26
|
}
|
|
28
27
|
discoveredProjects.add(projectId);
|
|
29
|
-
try {
|
|
30
|
-
await withAudit(projectPath, 'list_specs', 'discoverAndFlattenSpecs', () => discoverAndFlattenSpecs(projectPath));
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
/* best-effort — don't fail list_specs */
|
|
34
|
-
}
|
|
35
|
-
try {
|
|
36
|
-
await withAudit(projectPath, 'list_specs', 'importFilesystemSpecs', () => importFilesystemSpecs(projectPath, {
|
|
37
|
-
listSpecs: specStore.listSpecs,
|
|
38
|
-
createSpec: specStore.createSpec,
|
|
39
|
-
}, projectId), (imported) => ({ imported: Array.isArray(imported) ? imported.length : 0 }));
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
/* best-effort */
|
|
43
|
-
}
|
|
44
|
-
try {
|
|
45
|
-
await withAudit(projectPath, 'list_specs', 'reconcileSpecPaths', () => reconcileSpecPaths(projectPath, {
|
|
46
|
-
listSpecs: specStore.listSpecs,
|
|
47
|
-
updateSpec: specStore.updateSpec,
|
|
48
|
-
}, projectId));
|
|
49
|
-
}
|
|
50
|
-
catch {
|
|
51
|
-
/* best-effort */
|
|
52
|
-
}
|
|
53
|
-
try {
|
|
54
|
-
const allStoredSpecs = await specStore.listSpecs(projectId);
|
|
55
|
-
const unprefixed = filterUnprefixedSpecs(allStoredSpecs);
|
|
56
|
-
if (unprefixed.length > 0) {
|
|
57
|
-
await withAudit(projectPath, 'list_specs', 'migrateSpecFolderNames', () => migrateSpecFolderNames(projectPath, unprefixed, {
|
|
58
|
-
listSpecs: specStore.listSpecs,
|
|
59
|
-
updateSpec: specStore.updateSpec,
|
|
60
|
-
}), (migrated) => ({ count: Array.isArray(migrated) ? migrated.length : 0 }));
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
64
|
-
/* best-effort */
|
|
65
|
-
}
|
|
66
28
|
// SPEC-1017: list_specs enforces the strict planu/ policy in check mode.
|
|
67
29
|
// Mutating cleanup is handled by init/update/validate/housekeeping; list_specs
|
|
68
30
|
// stays read-mostly and surfaces exact offenders if any remain.
|
|
@@ -88,21 +50,6 @@ async function autoDiscoverProject(projectId, projectPath, collector) {
|
|
|
88
50
|
catch {
|
|
89
51
|
/* best-effort — never block list_specs */
|
|
90
52
|
}
|
|
91
|
-
try {
|
|
92
|
-
const { configureGitignoreForPlanu } = await import('./init-project/git-setup.js');
|
|
93
|
-
await withAudit(projectPath, 'list_specs', 'configureGitignoreForPlanu', () => configureGitignoreForPlanu(projectPath));
|
|
94
|
-
collector.pushOk('gitignore', 'Updated .gitignore with planu/ rules');
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
/* best-effort */
|
|
98
|
-
}
|
|
99
|
-
// SPEC-493: Silently repair status.json totalSpecs count from actual store data
|
|
100
|
-
try {
|
|
101
|
-
await withAudit(projectPath, 'list_specs', 'refreshProjectStatus', () => refreshProjectStatus(projectPath, projectId));
|
|
102
|
-
}
|
|
103
|
-
catch {
|
|
104
|
-
/* best-effort */
|
|
105
|
-
}
|
|
106
53
|
}
|
|
107
54
|
export async function handleListSpecs(params) {
|
|
108
55
|
// SPEC-584: list_specs is read-mostly and its migration-issue questions are opt-in.
|
|
@@ -152,9 +99,10 @@ export async function handleListSpecs(params) {
|
|
|
152
99
|
if (isNativeActive() && knowledge.projectPath) {
|
|
153
100
|
nativeBriefs = fastScanSpecs(knowledge.projectPath);
|
|
154
101
|
}
|
|
155
|
-
//
|
|
102
|
+
// Read-only project signals only. Mutating discovery/migration/repair belongs
|
|
103
|
+
// to init, validate, housekeeping, or explicit migration tools.
|
|
156
104
|
if (knowledge.projectPath) {
|
|
157
|
-
await
|
|
105
|
+
await collectReadOnlyProjectSignals(projectId, knowledge.projectPath, collector);
|
|
158
106
|
}
|
|
159
107
|
// Get all specs
|
|
160
108
|
let specs = await specStore.listSpecs(projectId);
|
|
@@ -247,6 +195,8 @@ export async function handleListSpecs(params) {
|
|
|
247
195
|
updatedAt: s.updatedAt,
|
|
248
196
|
lastModified: lastModifiedMap.get(s.id) ?? '\u2014',
|
|
249
197
|
}));
|
|
198
|
+
const structuredSpecs = specsData.slice(0, MAX_STRUCTURED_SPECS);
|
|
199
|
+
const specsOmitted = Math.max(0, specsData.length - structuredSpecs.length);
|
|
250
200
|
// Build text content: markdown table in summary mode, JSON in full mode
|
|
251
201
|
let textContent;
|
|
252
202
|
if (detail === 'summary') {
|
|
@@ -262,7 +212,13 @@ export async function handleListSpecs(params) {
|
|
|
262
212
|
},
|
|
263
213
|
count: specs.length,
|
|
264
214
|
totalCount: allSpecs.length,
|
|
265
|
-
specs:
|
|
215
|
+
specs: structuredSpecs,
|
|
216
|
+
pagination: {
|
|
217
|
+
limit: MAX_STRUCTURED_SPECS,
|
|
218
|
+
returned: structuredSpecs.length,
|
|
219
|
+
total: specsData.length,
|
|
220
|
+
omitted: specsOmitted,
|
|
221
|
+
},
|
|
266
222
|
summary: {
|
|
267
223
|
byStatus: statusCounts,
|
|
268
224
|
byType: typeCounts,
|