gsdd-cli 0.19.2 → 0.20.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/README.md +1 -0
- package/agents/DISTILLATION.md +16 -4
- package/agents/README.md +12 -0
- package/agents/approach-explorer.md +4 -4
- package/agents/executor.md +3 -0
- package/agents/integration-checker.md +2 -2
- package/agents/mapper.md +2 -2
- package/agents/planner.md +13 -1
- package/agents/researcher.md +2 -2
- package/agents/roadmapper.md +3 -1
- package/agents/synthesizer.md +2 -2
- package/agents/verifier.md +2 -0
- package/bin/gsdd.mjs +4 -5
- package/bin/lib/health.mjs +21 -3
- package/bin/lib/init-runtime.mjs +3 -0
- package/bin/lib/lifecycle-state.mjs +2 -2
- package/bin/lib/rendering.mjs +5 -0
- package/bin/lib/templates.mjs +1 -1
- package/bin/lib/ui-proof.mjs +467 -0
- package/distilled/DESIGN.md +88 -7
- package/distilled/EVIDENCE-INDEX.md +14 -0
- package/distilled/templates/delegates/approach-explorer.md +1 -1
- package/distilled/templates/delegates/mapper-arch.md +1 -1
- package/distilled/templates/delegates/mapper-concerns.md +1 -1
- package/distilled/templates/delegates/mapper-quality.md +1 -1
- package/distilled/templates/delegates/mapper-tech.md +1 -1
- package/distilled/templates/delegates/plan-checker.md +4 -2
- package/distilled/templates/delegates/researcher-architecture.md +1 -1
- package/distilled/templates/delegates/researcher-features.md +1 -1
- package/distilled/templates/delegates/researcher-pitfalls.md +1 -1
- package/distilled/templates/delegates/researcher-stack.md +1 -1
- package/distilled/templates/delegates/researcher-synthesizer.md +2 -2
- package/distilled/templates/ui-proof.md +174 -0
- package/distilled/workflows/audit-milestone.md +2 -1
- package/distilled/workflows/execute.md +7 -8
- package/distilled/workflows/map-codebase.md +4 -4
- package/distilled/workflows/new-project.md +8 -8
- package/distilled/workflows/plan.md +12 -11
- package/distilled/workflows/quick.md +6 -0
- package/distilled/workflows/verify.md +6 -4
- package/package.json +1 -1
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { isAbsolute, join, relative, resolve } from 'path';
|
|
3
|
+
import { output } from './cli-utils.mjs';
|
|
4
|
+
import { resolveWorkspaceContext } from './workspace-root.mjs';
|
|
5
|
+
|
|
6
|
+
const EVIDENCE_KINDS = Object.freeze(['code', 'test', 'runtime', 'delivery', 'human']);
|
|
7
|
+
const COMPARISON_STATUSES = Object.freeze(['satisfied', 'partial', 'missing', 'waived', 'deferred', 'not_applicable']);
|
|
8
|
+
const CLAIM_STATUSES = Object.freeze(['passed', 'failed', 'partial', 'waived', 'deferred', 'not_applicable']);
|
|
9
|
+
const ARTIFACT_VISIBILITIES = Object.freeze(['local_only', 'repo_tracked', 'public']);
|
|
10
|
+
const RAW_ARTIFACT_TYPES = Object.freeze(['screenshot', 'trace', 'video', 'dom_snapshot', 'dom-snapshot', 'dom', 'report']);
|
|
11
|
+
const PUBLIC_CLAIM_USES = Object.freeze(['public', 'publication', 'tracked', 'delivery', 'release']);
|
|
12
|
+
const CLAIM_USES = Object.freeze([...PUBLIC_CLAIM_USES, 'local', 'local_only']);
|
|
13
|
+
const REQUIRED_BUNDLE_FIELDS = Object.freeze([
|
|
14
|
+
'proof_bundle_version',
|
|
15
|
+
'scope',
|
|
16
|
+
'route_state',
|
|
17
|
+
'environment',
|
|
18
|
+
'viewport',
|
|
19
|
+
'evidence_inputs',
|
|
20
|
+
'commands_or_manual_steps',
|
|
21
|
+
'observations',
|
|
22
|
+
'artifacts',
|
|
23
|
+
'privacy',
|
|
24
|
+
'result',
|
|
25
|
+
'claim_limits',
|
|
26
|
+
]);
|
|
27
|
+
const REQUIRED_SCOPE_FIELDS = Object.freeze(['work_item', 'claim', 'requirement_ids', 'slot_ids']);
|
|
28
|
+
const REQUIRED_ARTIFACT_FIELDS = Object.freeze(['visibility', 'retention', 'sensitivity', 'safe_to_publish']);
|
|
29
|
+
const REQUIRED_OBSERVATION_FIELDS = Object.freeze(['observation', 'claim', 'route_state', 'evidence_kind', 'artifact_refs', 'privacy', 'result', 'claim_limit']);
|
|
30
|
+
const REQUIRED_PRIVACY_FIELDS = Object.freeze(['data_classification', 'raw_artifacts_safe_to_publish', 'retention']);
|
|
31
|
+
|
|
32
|
+
class UiProofError extends Error {}
|
|
33
|
+
|
|
34
|
+
function fail(message) {
|
|
35
|
+
console.error(message);
|
|
36
|
+
throw new UiProofError(message);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isPlainObject(value) {
|
|
40
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function hasValue(value) {
|
|
44
|
+
if (value === undefined || value === null) return false;
|
|
45
|
+
if (typeof value === 'string') return value.trim() !== '';
|
|
46
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
47
|
+
if (isPlainObject(value)) return Object.keys(value).length > 0;
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function pathLabel(basePath, key) {
|
|
52
|
+
return basePath ? `${basePath}.${key}` : key;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function addError(errors, code, path, message, fix) {
|
|
56
|
+
errors.push({ code, path, message, fix });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function requireField(obj, field, path, errors) {
|
|
60
|
+
if (!isPlainObject(obj) || !hasValue(obj[field])) {
|
|
61
|
+
addError(errors, 'missing_required_field', pathLabel(path, field), `Missing required UI proof field: ${pathLabel(path, field)}`, 'Add the required field to the proof bundle metadata.');
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeArray(value) {
|
|
68
|
+
if (Array.isArray(value)) return value;
|
|
69
|
+
if (typeof value === 'string' && value.trim()) return [value.trim()];
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function artifactType(artifact) {
|
|
74
|
+
const explicit = typeof artifact.type === 'string' ? artifact.type.toLowerCase() : '';
|
|
75
|
+
const artifactPath = typeof artifact.path === 'string' ? artifact.path.toLowerCase() : '';
|
|
76
|
+
if (/screenshot|\.png$|\.jpe?g$|\.webp$/.test(artifactPath)) return 'screenshot';
|
|
77
|
+
if (/trace|\.zip$/.test(artifactPath)) return 'trace';
|
|
78
|
+
if (/video|\.mp4$|\.webm$|\.mov$/.test(artifactPath)) return 'video';
|
|
79
|
+
if (/dom|\.html?$/.test(artifactPath)) return 'dom_snapshot';
|
|
80
|
+
if (/report/.test(artifactPath)) return 'report';
|
|
81
|
+
return explicit;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isRawUiArtifact(artifact) {
|
|
85
|
+
return RAW_ARTIFACT_TYPES.includes(artifactType(artifact));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function collectClaimUses(bundle, options) {
|
|
89
|
+
const uses = new Set();
|
|
90
|
+
for (const value of normalizeArray(options.claimUse).concat(normalizeArray(options.claimUses))) {
|
|
91
|
+
uses.add(String(value).toLowerCase());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const explicitSources = [
|
|
95
|
+
bundle?.proof_claim,
|
|
96
|
+
bundle?.proof_claims,
|
|
97
|
+
bundle?.claim_context?.proof_use,
|
|
98
|
+
bundle?.claim_context?.proof_uses,
|
|
99
|
+
bundle?.publication?.intended_use,
|
|
100
|
+
];
|
|
101
|
+
for (const source of explicitSources) {
|
|
102
|
+
for (const value of normalizeArray(source)) uses.add(String(value).toLowerCase());
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return [...uses];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function validateClaimUses(bundle, options, errors) {
|
|
109
|
+
for (const value of collectClaimUses(bundle, options)) {
|
|
110
|
+
if (!CLAIM_USES.includes(value)) {
|
|
111
|
+
addError(errors, 'unsupported_claim_use', 'proof_claim', `Unsupported UI proof claim use: ${value}`, `Use only: ${CLAIM_USES.join(', ')}.`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function hasPublicClaim(bundle, options) {
|
|
117
|
+
return collectClaimUses(bundle, options).some((value) => PUBLIC_CLAIM_USES.includes(value));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function validateObservationPrivacy(privacy, path, errors) {
|
|
121
|
+
for (const field of REQUIRED_PRIVACY_FIELDS) requireField(privacy, field, path, errors);
|
|
122
|
+
if (hasValue(privacy?.raw_artifacts_safe_to_publish) && typeof privacy.raw_artifacts_safe_to_publish !== 'boolean') {
|
|
123
|
+
addError(errors, 'invalid_raw_artifacts_safe_to_publish', `${path}.raw_artifacts_safe_to_publish`, 'raw_artifacts_safe_to_publish must be a boolean.', 'Use false unless all raw artifacts are explicitly safe to publish.');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function validateCommandsOrManualSteps(bundle, errors) {
|
|
128
|
+
for (const [index, step] of normalizeArray(bundle?.commands_or_manual_steps).entries()) {
|
|
129
|
+
const stepPath = `commands_or_manual_steps[${index}]`;
|
|
130
|
+
if (!isPlainObject(step)) {
|
|
131
|
+
addError(errors, 'invalid_proof_step', stepPath, 'UI proof command/manual step entry must be an object.', 'Record a command or manual_step plus its result.');
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (!hasValue(step.command) && !hasValue(step.manual_step)) {
|
|
135
|
+
addError(errors, 'missing_proof_step_action', stepPath, 'UI proof command/manual step must include command or manual_step.', 'Record the exact command or manual step used to generate the observation.');
|
|
136
|
+
}
|
|
137
|
+
if (!hasValue(step.result)) {
|
|
138
|
+
addError(errors, 'missing_proof_step_result', `${stepPath}.result`, 'UI proof command/manual step must include result.', `Record result using: ${CLAIM_STATUSES.join(', ')}.`);
|
|
139
|
+
} else if (!CLAIM_STATUSES.includes(step.result)) {
|
|
140
|
+
addError(errors, 'invalid_proof_step_result', `${stepPath}.result`, `Invalid UI proof command/manual step result: ${step.result}`, `Use only: ${CLAIM_STATUSES.join(', ')}.`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function validateObservations(bundle, errors) {
|
|
146
|
+
for (const [index, observation] of normalizeArray(bundle?.observations).entries()) {
|
|
147
|
+
const observationPath = `observations[${index}]`;
|
|
148
|
+
if (!isPlainObject(observation)) {
|
|
149
|
+
addError(errors, 'invalid_observation', observationPath, 'UI proof observation entry must be an object.', 'Record observation metadata with claim, route_state, evidence_kind, artifact_refs, privacy, result, and claim_limit.');
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
for (const field of REQUIRED_OBSERVATION_FIELDS) requireField(observation, field, observationPath, errors);
|
|
153
|
+
if (hasValue(observation.evidence_kind) && !EVIDENCE_KINDS.includes(observation.evidence_kind)) {
|
|
154
|
+
addError(errors, 'unsupported_evidence_kind', `${observationPath}.evidence_kind`, `Unsupported UI proof observation evidence kind: ${observation.evidence_kind}`, `Use only: ${EVIDENCE_KINDS.join(', ')}.`);
|
|
155
|
+
}
|
|
156
|
+
if (hasValue(observation.result) && !CLAIM_STATUSES.includes(observation.result)) {
|
|
157
|
+
addError(errors, 'invalid_observation_result', `${observationPath}.result`, `Invalid UI proof observation result: ${observation.result}`, `Use only: ${CLAIM_STATUSES.join(', ')}.`);
|
|
158
|
+
}
|
|
159
|
+
validateObservationPrivacy(observation.privacy, `${observationPath}.privacy`, errors);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function validateEvidenceKinds(bundle, errors) {
|
|
164
|
+
const kinds = normalizeArray(bundle?.evidence_inputs?.kinds);
|
|
165
|
+
if (kinds.length === 0) {
|
|
166
|
+
addError(errors, 'missing_evidence_kinds', 'evidence_inputs.kinds', 'Missing UI proof evidence kinds.', 'Record at least one fixed evidence kind: code, test, runtime, delivery, or human.');
|
|
167
|
+
}
|
|
168
|
+
for (const [index, kind] of kinds.entries()) {
|
|
169
|
+
if (!EVIDENCE_KINDS.includes(kind)) {
|
|
170
|
+
addError(errors, 'unsupported_evidence_kind', `evidence_inputs.kinds[${index}]`, `Unsupported UI proof evidence kind: ${kind}`, `Use only: ${EVIDENCE_KINDS.join(', ')}.`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function validateResult(bundle, errors) {
|
|
176
|
+
if (!isPlainObject(bundle?.result)) return;
|
|
177
|
+
if (!hasValue(bundle.result.claim_status)) {
|
|
178
|
+
addError(errors, 'missing_claim_status', 'result.claim_status', 'Missing UI proof result claim status.', `Record claim_status using: ${CLAIM_STATUSES.join(', ')}.`);
|
|
179
|
+
} else if (!CLAIM_STATUSES.includes(bundle.result.claim_status)) {
|
|
180
|
+
addError(errors, 'invalid_claim_status', 'result.claim_status', `Invalid UI proof claim status: ${bundle.result.claim_status}`, `Use only: ${CLAIM_STATUSES.join(', ')}.`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function validateComparisonStatuses(bundle, errors) {
|
|
185
|
+
const statuses = bundle?.result?.comparison_status_by_slot;
|
|
186
|
+
if (!isPlainObject(statuses)) {
|
|
187
|
+
addError(errors, 'missing_comparison_statuses', 'result.comparison_status_by_slot', 'Missing UI proof comparison statuses by slot.', `Record one status per slot using: ${COMPARISON_STATUSES.join(', ')}.`);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const slotIds = normalizeArray(bundle?.scope?.slot_ids);
|
|
191
|
+
const slotSet = new Set(slotIds);
|
|
192
|
+
for (const slotId of slotIds) {
|
|
193
|
+
if (!hasValue(statuses[slotId])) {
|
|
194
|
+
addError(errors, 'missing_comparison_status', `result.comparison_status_by_slot.${slotId}`, `Missing UI proof comparison status for slot: ${slotId}`, `Record one status per slot using: ${COMPARISON_STATUSES.join(', ')}.`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
for (const [slot, status] of Object.entries(statuses)) {
|
|
198
|
+
if (slotSet.size > 0 && !slotSet.has(slot)) {
|
|
199
|
+
addError(errors, 'unknown_comparison_slot', `result.comparison_status_by_slot.${slot}`, `UI proof comparison status references undeclared slot: ${slot}`, 'Use only slot IDs declared in scope.slot_ids.');
|
|
200
|
+
}
|
|
201
|
+
if (!COMPARISON_STATUSES.includes(status)) {
|
|
202
|
+
addError(errors, 'invalid_comparison_status', `result.comparison_status_by_slot.${slot}`, `Invalid UI proof comparison status: ${status}`, `Use only: ${COMPARISON_STATUSES.join(', ')}.`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function validateClaimLimits(bundle, errors) {
|
|
208
|
+
const claimLimits = normalizeArray(bundle?.claim_limits);
|
|
209
|
+
if (claimLimits.length === 0) {
|
|
210
|
+
addError(errors, 'missing_claim_limits', 'claim_limits', 'Missing UI proof claim limits.', 'Add at least one claim limit that narrows what this proof does not prove.');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function artifactReference(artifact) {
|
|
215
|
+
if (!isPlainObject(artifact)) return null;
|
|
216
|
+
if (typeof artifact.path === 'string' && artifact.path.trim()) return artifact.path.trim();
|
|
217
|
+
if (typeof artifact.url === 'string' && artifact.url.trim()) return artifact.url.trim();
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function validateArtifactReferenceSafety(ref, path, errors) {
|
|
222
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(ref)) {
|
|
223
|
+
if (!/^https?:\/\//i.test(ref)) {
|
|
224
|
+
addError(errors, 'invalid_artifact_ref_location', path, `UI proof artifact reference uses an unsupported URL scheme: ${ref}`, 'Use a workspace-relative path or an http(s) URL; do not reference local file URLs.');
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (ref.startsWith('//') || isAbsolute(ref) || ref.split(/[\\/]+/).includes('..')) {
|
|
229
|
+
addError(errors, 'invalid_artifact_ref_location', path, `UI proof artifact reference must stay workspace-relative: ${ref}`, 'Use a relative path under the workspace, or an http(s) URL for external sanitized evidence.');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function isSanitizedSensitivity(value) {
|
|
234
|
+
return typeof value === 'string' && /(^|[_\s-])(sanitized|public_safe|public-safe)($|[_\s-])/.test(value.toLowerCase());
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function validateArtifacts(bundle, errors, publicClaim) {
|
|
238
|
+
const artifacts = normalizeArray(bundle?.artifacts);
|
|
239
|
+
if (artifacts.length === 0) {
|
|
240
|
+
addError(errors, 'missing_artifacts', 'artifacts', 'Missing UI proof artifacts list.', 'Record artifact metadata for each referenced proof artifact.');
|
|
241
|
+
return new Set();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const artifactRefs = new Set();
|
|
245
|
+
for (const [index, artifact] of artifacts.entries()) {
|
|
246
|
+
const artifactPath = `artifacts[${index}]`;
|
|
247
|
+
if (!isPlainObject(artifact)) {
|
|
248
|
+
addError(errors, 'invalid_artifact', artifactPath, 'UI proof artifact entry must be an object.', 'Record path/type plus privacy metadata for each artifact.');
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const ref = artifactReference(artifact);
|
|
252
|
+
if (!ref) {
|
|
253
|
+
addError(errors, 'missing_artifact_ref', artifactPath, 'UI proof artifact must include path or url.', 'Reference raw UI artifacts by path or URL; do not inline them.');
|
|
254
|
+
} else {
|
|
255
|
+
validateArtifactReferenceSafety(ref, artifactPath, errors);
|
|
256
|
+
artifactRefs.add(ref);
|
|
257
|
+
}
|
|
258
|
+
for (const field of REQUIRED_ARTIFACT_FIELDS) {
|
|
259
|
+
requireField(artifact, field, artifactPath, errors);
|
|
260
|
+
}
|
|
261
|
+
if (hasValue(artifact.visibility) && !ARTIFACT_VISIBILITIES.includes(artifact.visibility)) {
|
|
262
|
+
addError(errors, 'invalid_visibility', `${artifactPath}.visibility`, `Invalid UI proof artifact visibility: ${artifact.visibility}`, `Use only: ${ARTIFACT_VISIBILITIES.join(', ')}.`);
|
|
263
|
+
}
|
|
264
|
+
if (hasValue(artifact.safe_to_publish) && typeof artifact.safe_to_publish !== 'boolean') {
|
|
265
|
+
addError(errors, 'invalid_safe_to_publish', `${artifactPath}.safe_to_publish`, 'safe_to_publish must be a boolean.', 'Use true only after explicit safe-to-publish classification; otherwise use false.');
|
|
266
|
+
}
|
|
267
|
+
if (isRawUiArtifact(artifact) && artifact.visibility !== 'local_only' && artifact.safe_to_publish !== true) {
|
|
268
|
+
addError(errors, 'unsafe_raw_artifact', artifactPath, 'Raw UI artifacts are local-only by default unless explicitly classified safe to publish.', 'Set visibility: local_only and safe_to_publish: false, or document sanitized public-safe classification.');
|
|
269
|
+
}
|
|
270
|
+
if (publicClaim && (artifact.visibility === 'local_only' || artifact.safe_to_publish !== true)) {
|
|
271
|
+
addError(errors, 'unsafe_public_proof_claim', artifactPath, 'Public/tracked/delivery UI proof claims cannot rely on local-only or unsafe artifacts.', 'Use local-only claim language, or provide sanitized artifacts with safe_to_publish: true and non-local visibility.');
|
|
272
|
+
}
|
|
273
|
+
if (publicClaim && isRawUiArtifact(artifact) && !isSanitizedSensitivity(artifact.sensitivity)) {
|
|
274
|
+
addError(errors, 'unsafe_public_artifact_sensitivity', `${artifactPath}.sensitivity`, 'Public/tracked/delivery raw UI proof artifacts must be classified sanitized.', 'Set sensitivity to a sanitized/public-safe classification after explicit review, or narrow the proof claim.');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return artifactRefs;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function validatePrivacy(bundle, errors, publicClaim) {
|
|
281
|
+
validateObservationPrivacy(bundle.privacy, 'privacy', errors);
|
|
282
|
+
if (publicClaim && bundle.privacy?.raw_artifacts_safe_to_publish !== true) {
|
|
283
|
+
addError(errors, 'unsafe_public_proof_privacy', 'privacy.raw_artifacts_safe_to_publish', 'Public/tracked/delivery UI proof claims require bundle privacy metadata to classify raw artifacts safe to publish.', 'Use local-only claim language, or set raw_artifacts_safe_to_publish: true after sanitized/public-safe review.');
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function validatePublicObservationPrivacy(bundle, errors, publicClaim) {
|
|
288
|
+
if (!publicClaim) return;
|
|
289
|
+
for (const [index, observation] of normalizeArray(bundle?.observations).entries()) {
|
|
290
|
+
if (!isPlainObject(observation)) continue;
|
|
291
|
+
if (observation.privacy?.raw_artifacts_safe_to_publish !== true) {
|
|
292
|
+
addError(errors, 'unsafe_public_observation_privacy', `observations[${index}].privacy.raw_artifacts_safe_to_publish`, 'Public/tracked/delivery UI proof claims require observation privacy metadata to classify raw artifacts safe to publish.', 'Use local-only claim language, or set raw_artifacts_safe_to_publish: true after sanitized/public-safe review.');
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function validateObservationArtifactRefs(bundle, artifactRefs, errors) {
|
|
298
|
+
for (const [index, observation] of normalizeArray(bundle?.observations).entries()) {
|
|
299
|
+
if (!isPlainObject(observation)) continue;
|
|
300
|
+
for (const [refIndex, ref] of normalizeArray(observation.artifact_refs).entries()) {
|
|
301
|
+
if (!artifactRefs.has(ref)) {
|
|
302
|
+
addError(errors, 'unknown_artifact_ref', `observations[${index}].artifact_refs[${refIndex}]`, `Observation references undeclared UI proof artifact: ${ref}`, 'Add the artifact to artifacts[] or correct the observation artifact reference.');
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function validateUiProofBundle(bundle, options = {}) {
|
|
309
|
+
const errors = [];
|
|
310
|
+
const warnings = [];
|
|
311
|
+
|
|
312
|
+
if (!isPlainObject(bundle)) {
|
|
313
|
+
addError(errors, 'invalid_bundle', '', 'UI proof bundle must be an object.', 'Provide structured UI proof metadata.');
|
|
314
|
+
return { valid: false, errors, warnings };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
for (const field of REQUIRED_BUNDLE_FIELDS) requireField(bundle, field, '', errors);
|
|
318
|
+
for (const field of REQUIRED_SCOPE_FIELDS) requireField(bundle.scope, field, 'scope', errors);
|
|
319
|
+
const publicClaim = hasPublicClaim(bundle, options);
|
|
320
|
+
validateClaimUses(bundle, options, errors);
|
|
321
|
+
validateEvidenceKinds(bundle, errors);
|
|
322
|
+
validateCommandsOrManualSteps(bundle, errors);
|
|
323
|
+
validateObservations(bundle, errors);
|
|
324
|
+
validateResult(bundle, errors);
|
|
325
|
+
validateComparisonStatuses(bundle, errors);
|
|
326
|
+
validateClaimLimits(bundle, errors);
|
|
327
|
+
validatePrivacy(bundle, errors, publicClaim);
|
|
328
|
+
validatePublicObservationPrivacy(bundle, errors, publicClaim);
|
|
329
|
+
const artifactRefs = validateArtifacts(bundle, errors, publicClaim);
|
|
330
|
+
validateObservationArtifactRefs(bundle, artifactRefs, errors);
|
|
331
|
+
|
|
332
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function parseUiProofBundleContent(content, filePath = 'UI proof bundle') {
|
|
336
|
+
const trimmed = content.trim();
|
|
337
|
+
if (!trimmed) {
|
|
338
|
+
return { bundle: null, errors: [{ code: 'empty_bundle_file', path: filePath, message: 'UI proof bundle file is empty.', fix: 'Write JSON UI proof metadata before validating.' }] };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const jsonCandidates = [trimmed];
|
|
342
|
+
const fenceMatches = [...trimmed.matchAll(/```(?:json|ui-proof-json)?\s*([\s\S]*?)```/gi)];
|
|
343
|
+
for (const match of fenceMatches) jsonCandidates.push(match[1].trim());
|
|
344
|
+
|
|
345
|
+
for (const candidate of jsonCandidates) {
|
|
346
|
+
try {
|
|
347
|
+
return { bundle: JSON.parse(candidate), errors: [] };
|
|
348
|
+
} catch {
|
|
349
|
+
// Try next candidate; final error is reported below.
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
bundle: null,
|
|
355
|
+
errors: [{ code: 'unparseable_bundle', path: filePath, message: 'UI proof bundle metadata is not valid JSON.', fix: 'Use a .json proof bundle or a markdown fenced JSON block; no YAML parser dependency is installed.' }],
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export function readUiProofBundleFile(filePath) {
|
|
360
|
+
return parseUiProofBundleContent(readFileSync(filePath, 'utf-8'), filePath);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function walkForUiProofFiles(dir, results) {
|
|
364
|
+
if (!existsSync(dir)) return;
|
|
365
|
+
for (const entry of readdirSync(dir)) {
|
|
366
|
+
const fullPath = join(dir, entry);
|
|
367
|
+
const stat = statSync(fullPath);
|
|
368
|
+
if (stat.isDirectory()) {
|
|
369
|
+
walkForUiProofFiles(fullPath, results);
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
const name = entry.toLowerCase();
|
|
373
|
+
if (['ui-proof.json', 'ui-proof.md', 'proof-bundle.json'].includes(name)) {
|
|
374
|
+
results.add(fullPath);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export function findUiProofBundleFiles(planningDir) {
|
|
380
|
+
const results = new Set();
|
|
381
|
+
for (const relativePath of [
|
|
382
|
+
'UI-PROOF.json',
|
|
383
|
+
'ui-proof.json',
|
|
384
|
+
'ui-proof.md',
|
|
385
|
+
'ui-proof/UI-PROOF.json',
|
|
386
|
+
'ui-proof/proof-bundle.json',
|
|
387
|
+
'brownfield-change/UI-PROOF.json',
|
|
388
|
+
]) {
|
|
389
|
+
const fullPath = join(planningDir, relativePath);
|
|
390
|
+
if (existsSync(fullPath)) results.add(fullPath);
|
|
391
|
+
}
|
|
392
|
+
for (const relativeDir of ['phases', 'quick', 'brownfield-change']) {
|
|
393
|
+
walkForUiProofFiles(join(planningDir, relativeDir), results);
|
|
394
|
+
}
|
|
395
|
+
return [...results].sort();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function resolveWorkspacePath(cwd, target) {
|
|
399
|
+
const workspaceRoot = resolve(cwd);
|
|
400
|
+
const resolved = resolve(workspaceRoot, target);
|
|
401
|
+
const rel = relative(workspaceRoot, resolved);
|
|
402
|
+
if (rel === '' || (!rel.startsWith('..') && !isAbsolute(rel))) return resolved;
|
|
403
|
+
fail(`Path must stay inside the workspace: ${target}`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function parseClaimUse(args) {
|
|
407
|
+
const values = [];
|
|
408
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
409
|
+
const arg = args[index];
|
|
410
|
+
if (arg !== '--claim') fail('Usage: gsdd ui-proof validate <path> [--claim <public|publication|tracked|delivery|release>]');
|
|
411
|
+
const value = args[index + 1];
|
|
412
|
+
if (!value || value.startsWith('--')) fail('Usage: gsdd ui-proof validate <path> [--claim <public|publication|tracked|delivery|release>]');
|
|
413
|
+
values.push(...value.split(',').map((entry) => entry.trim()).filter(Boolean));
|
|
414
|
+
index += 1;
|
|
415
|
+
}
|
|
416
|
+
for (const value of values) {
|
|
417
|
+
if (!PUBLIC_CLAIM_USES.includes(value)) fail(`Unsupported UI proof claim use: ${value}`);
|
|
418
|
+
}
|
|
419
|
+
return values;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function cmdValidate(cwd, args) {
|
|
423
|
+
const [targetArg, ...flags] = args;
|
|
424
|
+
if (!targetArg) fail('Usage: gsdd ui-proof validate <path> [--claim <public|publication|tracked|delivery|release>]');
|
|
425
|
+
const target = resolveWorkspacePath(cwd, targetArg);
|
|
426
|
+
if (!existsSync(target) || statSync(target).isDirectory()) fail(`UI proof bundle file does not exist: ${targetArg}`);
|
|
427
|
+
|
|
428
|
+
const parsed = readUiProofBundleFile(target);
|
|
429
|
+
const validation = parsed.errors.length > 0
|
|
430
|
+
? { valid: false, errors: parsed.errors, warnings: [] }
|
|
431
|
+
: validateUiProofBundle(parsed.bundle, { claimUses: parseClaimUse(flags) });
|
|
432
|
+
|
|
433
|
+
output({ operation: 'ui-proof validate', target: targetArg, valid: validation.valid, errors: validation.errors, warnings: validation.warnings });
|
|
434
|
+
if (!validation.valid) process.exitCode = 1;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export function cmdUiProof(...args) {
|
|
438
|
+
const { args: normalizedArgs, workspaceRoot, invalid, error } = resolveWorkspaceContext(args);
|
|
439
|
+
if (invalid) {
|
|
440
|
+
console.error(error);
|
|
441
|
+
process.exitCode = 1;
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const [operation, ...rest] = normalizedArgs;
|
|
445
|
+
try {
|
|
446
|
+
switch (operation) {
|
|
447
|
+
case 'validate':
|
|
448
|
+
cmdValidate(workspaceRoot, rest);
|
|
449
|
+
return;
|
|
450
|
+
default:
|
|
451
|
+
fail('Usage: gsdd ui-proof validate <path> [--claim <public|publication|tracked|delivery|release>]');
|
|
452
|
+
}
|
|
453
|
+
} catch (error) {
|
|
454
|
+
if (error instanceof UiProofError) {
|
|
455
|
+
process.exitCode = 1;
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
throw error;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export {
|
|
463
|
+
ARTIFACT_VISIBILITIES as UI_PROOF_ARTIFACT_VISIBILITIES,
|
|
464
|
+
COMPARISON_STATUSES as UI_PROOF_COMPARISON_STATUSES,
|
|
465
|
+
EVIDENCE_KINDS as UI_PROOF_EVIDENCE_KINDS,
|
|
466
|
+
RAW_ARTIFACT_TYPES as UI_PROOF_RAW_ARTIFACT_TYPES,
|
|
467
|
+
};
|
package/distilled/DESIGN.md
CHANGED
|
@@ -71,6 +71,8 @@
|
|
|
71
71
|
58. [Local Workflow Helper Launcher](#d58---local-workflow-helper-launcher)
|
|
72
72
|
59. [Continuity Authority And Planning-State Drift](#d59---continuity-authority-and-planning-state-drift)
|
|
73
73
|
60. [Release Closeout Contract](#d60---release-closeout-contract)
|
|
74
|
+
61. [Deliberate Subagent Contract](#d61---deliberate-subagent-contract)
|
|
75
|
+
62. [Repo-Native UI Proof Contract](#d62---repo-native-ui-proof-contract)
|
|
74
76
|
|
|
75
77
|
---
|
|
76
78
|
|
|
@@ -285,11 +287,11 @@ This hardening pass also clarified a reusable architectural rule: strict portabl
|
|
|
285
287
|
|
|
286
288
|
| researchDepth | Synthesizer behavior |
|
|
287
289
|
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
288
|
-
| `fast` | Orchestrator writes SUMMARY.md inline from the 4
|
|
290
|
+
| `fast` | Orchestrator writes SUMMARY.md inline from the 4 human-read structured summaries it holds in context. No delegate spawned. |
|
|
289
291
|
| `balanced` | ResearchSynthesizer delegate spawned. Reads 4 full research files. Cross-references build order constraints, pitfall-to-phase mappings, feature-architecture conflicts that short summaries omit. |
|
|
290
292
|
| `deep` | Same as balanced but researchers produce longer output (more material for synthesizer to cross-reference). |
|
|
291
293
|
|
|
292
|
-
**Why conditional:** The synthesizer's value is in cross-referencing specific data across research dimensions. When `researchDepth=fast`, researchers
|
|
294
|
+
**Why conditional:** The synthesizer's value is in cross-referencing specific data across research dimensions. When `researchDepth=fast`, researchers return compact summaries and the full research files remain available on disk; spawning a synthesizer to reformat shallow inputs wastes a context window and an agent hop.
|
|
293
295
|
|
|
294
296
|
**Evidence:**
|
|
295
297
|
|
|
@@ -442,7 +444,7 @@ Codex CLI is skills-first because the terminal CLI supports repository skills di
|
|
|
442
444
|
|
|
443
445
|
**GSDD:** Makes this explicit as a design rule.
|
|
444
446
|
|
|
445
|
-
**The rule:** Delegates write full documents to disk. They return
|
|
447
|
+
**The rule:** Delegates write full documents to disk. They return bounded summaries to the orchestrator: routing summaries use 100-200 tokens, human-read summaries use 300-500 tokens, and agent-mediated discussion summaries use 500-800 tokens. The orchestrator never receives document contents in its conversation context.
|
|
446
448
|
|
|
447
449
|
**Why this matters:**
|
|
448
450
|
|
|
@@ -452,7 +454,7 @@ Codex CLI is skills-first because the terminal CLI supports repository skills di
|
|
|
452
454
|
|
|
453
455
|
**Implementation:**
|
|
454
456
|
|
|
455
|
-
- Each delegate's instructions
|
|
457
|
+
- Each delegate's instructions identify the correct summary tier and keep full document contents on disk instead of echoing them into orchestrator context.
|
|
456
458
|
- Output templates in `.planning/templates/research/` and `.planning/templates/codebase/` define the on-disk format.
|
|
457
459
|
- The synthesizer reads all 4 research files from disk -- it is the only agent that sees full research content.
|
|
458
460
|
|
|
@@ -955,8 +957,9 @@ Implementation lives under `bin/lib/`:
|
|
|
955
957
|
| E5 | ERROR | `.planning/templates/delegates/` missing or empty |
|
|
956
958
|
| E6 | ERROR | `.planning/templates/research/` missing or empty |
|
|
957
959
|
| E7 | ERROR | `.planning/templates/codebase/` missing or empty |
|
|
958
|
-
| E8 | ERROR | `.planning/templates/` missing critical root files (`spec.md`, `roadmap.md`, `auth-matrix.md`) |
|
|
960
|
+
| E8 | ERROR | `.planning/templates/` missing critical root files (`spec.md`, `roadmap.md`, `auth-matrix.md`, `ui-proof.md`) |
|
|
959
961
|
| E9 | ERROR | `.planning/templates/brownfield-change/` missing or missing critical files (`CHANGE.md`, `HANDOFF.md`, `VERIFICATION.md`) |
|
|
962
|
+
| E10 | ERROR | Known UI proof bundle metadata is unparseable or fails deterministic privacy/claim validation |
|
|
960
963
|
| W1 | WARN | `generation-manifest.json` missing |
|
|
961
964
|
| W2 | WARN | Manifest-tracked installed templates/helpers modified locally (hash mismatch vs manifest) |
|
|
962
965
|
| W3 | WARN | Manifest-tracked installed templates/helpers missing from disk but listed in manifest |
|
|
@@ -1356,7 +1359,7 @@ Combined, these three workflows provided genuine leverage: the planner could not
|
|
|
1356
1359
|
|
|
1357
1360
|
**GSDD decision:** Recover the discuss-phase leverage as a single role (`agents/approach-explorer.md`) embedded in the plan workflow, with a hybrid interaction architecture:
|
|
1358
1361
|
|
|
1359
|
-
1. **Primary path (inline + research subagents):** Conversation runs in the plan workflow's main context (required for user interactivity). For each technical gray area, a read-only research subagent spawns, reads codebase/docs, and returns a compressed
|
|
1362
|
+
1. **Primary path (inline + research subagents):** Conversation runs in the plan workflow's main context (required for user interactivity). For each technical gray area, a read-only research subagent spawns, reads codebase/docs, and returns a compressed 500-800 token structured summary. Only summaries enter the conversation context, not raw file reads.
|
|
1360
1363
|
|
|
1361
1364
|
2. **Native agent optimization:** Runtimes with interactive subagent support (Claude Code with `AskUserQuestion`, Codex interactive agents, OpenCode `mode: agent`) can run the full exploration as a native agent. Falls back to the inline primary path if unavailable.
|
|
1362
1365
|
|
|
@@ -1389,7 +1392,7 @@ The approach explorer needs two capabilities with opposite context requirements:
|
|
|
1389
1392
|
- **Conversation** needs the main context (for user interaction)
|
|
1390
1393
|
- **Research** generates thousands of tokens of raw content the conversation doesn't need
|
|
1391
1394
|
|
|
1392
|
-
Isolating research in subagents and returning compressed summaries follows the Compress and Isolate patterns from context engineering literature. The research subagent prompt template lives in the role contract (`<research_subagent_prompt>` section of `agents/approach-explorer.md`) — co-located with the algorithm it serves, and referenced by the portable workflow rather than inlined. The main context budget stays manageable:
|
|
1395
|
+
Isolating research in subagents and returning compressed summaries follows the Compress and Isolate patterns from context engineering literature. The research subagent prompt template lives in the role contract (`<research_subagent_prompt>` section of `agents/approach-explorer.md`) — co-located with the algorithm it serves, and referenced by the portable workflow rather than inlined. The main context budget stays manageable: about 1000 tokens orchestration, up to 3200 tokens research summaries (4 areas x 800), about 4000 tokens conversation, and about 500 tokens APPROACH.md. The 500-800 token agent-mediated discussion tier gives research subagents room for the structured format (Name/Pro/Con/Source) plus recommendation reasoning, source verification, and enough project-specific context that the main agent can handle follow-up questions without re-querying the subagent.
|
|
1393
1396
|
|
|
1394
1397
|
**Evidence:**
|
|
1395
1398
|
|
|
@@ -2776,6 +2779,84 @@ Posture compatibility is part of that closeout contract: `repo_closeout` and `ru
|
|
|
2776
2779
|
- `complete-milestone` now fails closed before archive writes when the passed audit omits or contradicts the release claim contract.
|
|
2777
2780
|
- Public claims are scoped to tracked public or repo-visible proof, while GitHub Releases, tags, package publication, and release automation remain deferred until explicitly planned.
|
|
2778
2781
|
|
|
2782
|
+
## D61 - Deliberate Subagent Contract
|
|
2783
|
+
|
|
2784
|
+
**Decision (2026-04-28):** Subagents are deliberate isolation tools, not hidden implementation orchestration. Workspine should use them for research, review, mapping, synthesis, and integration checking when the work is read-only or artifact-backed, while implementation remains sequential unless an approved plan explicitly owns disjoint write sets.
|
|
2785
|
+
|
|
2786
|
+
**Context:**
|
|
2787
|
+
- Phase 50 found that existing subagent wording was over-strong in some planning and role surfaces while summary-size guidance had drifted across 3-5 sentence, 300-500 token, and 1000-token variants.
|
|
2788
|
+
- Phase 46 research supported parallel agents for breadth-first research, but warned that coding writes need stronger coordination before parallel PR or multi-worktree orchestration is safe.
|
|
2789
|
+
- Phase 53 approach alignment confirmed the conservative boundary: allow read-heavy and artifact-backed delegation, forbid hidden implementation orchestration, keep roadmapper role-only/direct invocation, and do not claim broad S3/S5 closure or native runtime parity.
|
|
2790
|
+
|
|
2791
|
+
**Decision:**
|
|
2792
|
+
- Allowed subagent use: research, review, mapping, synthesis, and integration checks when they are read-only or when the subagent writes/updates its own bounded durable artifact.
|
|
2793
|
+
- Return discipline: routing summaries use 100-200 tokens, human-read summaries use 300-500 tokens, agent-mediated discussion summaries use 500-800 tokens, and full details live in durable documents on disk.
|
|
2794
|
+
- Forbidden by default: hidden implementation orchestration, agent teams, parallel PR flows, multi-worktree coding, and overlapping implementation writes without explicit write-set ownership in the approved plan.
|
|
2795
|
+
- Roadmapper remains role-only/direct invocation in Phase 53. Roadmap creation is sequential and coverage-sensitive, writes `.planning/ROADMAP.md`, and does not yet have a proven thin-wrapper delegate use case.
|
|
2796
|
+
- S3 and S5 remain deferred. This decision narrows the practical Workspine subagent contract but does not close broad subagent orchestration parity or adapter research gaps.
|
|
2797
|
+
|
|
2798
|
+
**Leverage:**
|
|
2799
|
+
- Lost: less flexibility for future agents to add delegates by symmetry or treat missing delegates as accidental gaps.
|
|
2800
|
+
- Kept: the two-layer role/delegate architecture, summaries-up/documents-to-disk, repo-native workflow state, lifecycle preflight, and write-set constraints.
|
|
2801
|
+
- Gained: a durable conservative rule that tells agents when subagents earn their keep and when they create hidden orchestration risk.
|
|
2802
|
+
|
|
2803
|
+
**Evidence:**
|
|
2804
|
+
- `agents/README.md`
|
|
2805
|
+
- `agents/DISTILLATION.md`
|
|
2806
|
+
- `distilled/templates/delegates/`
|
|
2807
|
+
- `distilled/workflows/map-codebase.md`
|
|
2808
|
+
- `distilled/workflows/new-project.md`
|
|
2809
|
+
- GSD comparison source: `get-shit-done/workflows/new-project.md` and mapper/research workflows use `Task()` subagents heavily, but GSDD keeps the portable core single-agent-safe and only preserves subagent use where isolation, artifact persistence, or fresh-context review pays for the complexity.
|
|
2810
|
+
|
|
2811
|
+
**Consequences:**
|
|
2812
|
+
- Future role, delegate, and workflow wording must distinguish read-only/artifact-backed delegation from implementation write ownership.
|
|
2813
|
+
- A roadmapper delegate must not appear by symmetry; it needs a future explicit design decision and regression updates.
|
|
2814
|
+
- Generated runtime freshness may propagate this wording, but generated freshness is not runtime parity proof.
|
|
2815
|
+
|
|
2816
|
+
---
|
|
2817
|
+
|
|
2818
|
+
## D62 - Repo-Native UI Proof Contract
|
|
2819
|
+
|
|
2820
|
+
**Decision (2026-04-28):** UI-sensitive work should carry a compact planned proof-slot contract and, when executed, an observed UI proof bundle that references artifacts by path or link while preserving the existing closure evidence kinds: `code`, `test`, `runtime`, `delivery`, and `human`.
|
|
2821
|
+
|
|
2822
|
+
**Context:**
|
|
2823
|
+
- UI proof targets the recurring failure mode where agents claim a UI works or looks good without rendered proof, matched observations, or explicit human judgment.
|
|
2824
|
+
- The contract defines proof slots, proof bundles, comparison statuses, fail-closed agent guardrails, deterministic metadata validation, privacy metadata, and health visibility without adding a browser-provider framework.
|
|
2825
|
+
- GSD's archived planner, executor, and verifier roles preserve strong lifecycle discipline, but they do not provide this UI-specific planned-vs-observed proof model. GSDD keeps the lifecycle leverage and adds a repo-native UI proof substrate without adding a browser-provider framework.
|
|
2826
|
+
|
|
2827
|
+
**Decision:**
|
|
2828
|
+
- Planning must classify UI-sensitive work and require either `ui_proof_slots` or an explicit `no_ui_proof_rationale`.
|
|
2829
|
+
- Planned slots record claim, route/state, required evidence kinds, minimum observations, environment/viewport, manual-acceptance requirement, claim limit, and requirement IDs.
|
|
2830
|
+
- Observed proof bundles record claim, requirement/slot IDs, route/state, environment, viewport, evidence inputs, commands/manual steps, observations, artifacts, privacy metadata, result, and claim limits.
|
|
2831
|
+
- Verification compares planned slots to observed bundles using `satisfied`, `partial`, `missing`, `waived`, `deferred`, and `not_applicable`; waiver and deferral are not proof.
|
|
2832
|
+
- UI correctness claims fail closed unless rendered proof is matched exactly to claim, route/state, observation, evidence kind, artifact path or manual step, privacy metadata, result, and claim limit, or an explicit waiver/deferment narrows the claim.
|
|
2833
|
+
- Human acceptance may close a narrowed claim and record proof debt, but it must not convert missing or mismatched non-human evidence into `satisfied` proof.
|
|
2834
|
+
- Screenshots, traces, videos, reports, accessibility scans, Gherkin, and visual diffs are artifact types or activities mapped onto the five existing evidence kinds, not new evidence kinds.
|
|
2835
|
+
- Source annotations, AST/cAST findings, semantic search hits, comments, and Semble-like retrieval may discover proof obligations, but they are discovery hints only and do not satisfy proof slots.
|
|
2836
|
+
- Visual taste, accessibility judgment, baseline acceptance, subjective polish/layout quality, and privacy publication require human evidence or explicit waiver, and human approval does not replace required `code`, `test`, `runtime`, or `delivery` evidence.
|
|
2837
|
+
- Deterministic metadata enforcement keeps the evidence and comparison-status vocabularies unchanged: artifact entries require `visibility`, `retention`, `sensitivity`, and `safe_to_publish`; raw screenshots, traces, videos, DOM snapshots, and reports default to `local_only` plus `safe_to_publish: false`; `bin/lib/ui-proof.mjs` validates required bundle/observation fields, structured command/manual-step entries, fixed evidence kinds, claim/result statuses, comparison statuses, claim limits, privacy metadata, safe artifact references, and public/tracked/delivery proof claims backed by local-only, unsafe, unsanitized, or privacy-contradictory artifacts.
|
|
2838
|
+
- `gsdd health` reports invalid known UI proof bundles as E10 using the same validator, staying read-only and metadata-only.
|
|
2839
|
+
|
|
2840
|
+
**Leverage:**
|
|
2841
|
+
- Lost: UI-sensitive work now carries a small proof-contract burden, and invalid proof metadata can degrade/break health before agents can claim rendered UI outcomes.
|
|
2842
|
+
- Kept: repo-native markdown artifacts, optional project tooling, fixed closure evidence kinds, generated-surface freshness, and the plan/execute/verify separation.
|
|
2843
|
+
- Gained: exact claim-to-proof traceability, strict comparison statuses, privacy and claim-limit metadata, fail-closed overclaim guardrails, deterministic metadata validation, and health-visible protection against unsafe public proof claims.
|
|
2844
|
+
|
|
2845
|
+
**Evidence:**
|
|
2846
|
+
- `distilled/templates/ui-proof.md`
|
|
2847
|
+
- `distilled/workflows/plan.md`, `distilled/workflows/execute.md`, `distilled/workflows/quick.md`, `distilled/workflows/verify.md`
|
|
2848
|
+
- `agents/planner.md`, `agents/executor.md`, `agents/verifier.md`, `distilled/templates/delegates/plan-checker.md`
|
|
2849
|
+
- `bin/lib/templates.mjs`, `bin/lib/ui-proof.mjs`, `bin/lib/health.mjs`, `bin/lib/rendering.mjs`
|
|
2850
|
+
- `tests/phase.test.cjs`, `tests/gsdd.guards.test.cjs`, `tests/gsdd.health.test.cjs`, `tests/gsdd.init.test.cjs`
|
|
2851
|
+
- GSD comparison: the upstream planner, executor, and verifier role patterns preserve lifecycle rigor, but they do not define UI proof slots or planned-vs-observed UI proof bundles.
|
|
2852
|
+
|
|
2853
|
+
**Consequences:**
|
|
2854
|
+
- Future UI-related phases must not add new evidence kinds by treating artifact types as proof categories.
|
|
2855
|
+
- Future dogfood or runtime validation must not upgrade artifact counts or human waivers into proof.
|
|
2856
|
+
- Generated runtime surfaces and local templates must stay freshness-checkable through `gsdd update --templates` and health diagnostics.
|
|
2857
|
+
|
|
2858
|
+
---
|
|
2859
|
+
|
|
2779
2860
|
## Maintenance
|
|
2780
2861
|
|
|
2781
2862
|
This document is updated when:
|
|
@@ -478,6 +478,20 @@
|
|
|
478
478
|
- `distilled/DESIGN.md` D50 and D59
|
|
479
479
|
- `tests/phase.test.cjs`, `tests/gsdd.guards.test.cjs`, `tests/gsdd.scenarios.test.cjs`
|
|
480
480
|
|
|
481
|
+
## D61 — Deliberate Subagent Contract
|
|
482
|
+
- `agents/README.md`, `agents/DISTILLATION.md`, `agents/approach-explorer.md`, `agents/planner.md`, `agents/roadmapper.md`
|
|
483
|
+
- `distilled/templates/delegates/approach-explorer.md`, `distilled/templates/delegates/plan-checker.md`
|
|
484
|
+
- `distilled/workflows/audit-milestone.md`, `distilled/workflows/map-codebase.md`, `distilled/workflows/new-project.md`, `distilled/workflows/plan.md`
|
|
485
|
+
- GSD comparison source: `get-shit-done/workflows/new-project.md`
|
|
486
|
+
- `tests/gsdd.guards.test.cjs`, `tests/gsdd.invariants.test.cjs`
|
|
487
|
+
|
|
488
|
+
## D62 — Repo-Native UI Proof Contract
|
|
489
|
+
- `distilled/templates/ui-proof.md`
|
|
490
|
+
- `distilled/workflows/plan.md`, `distilled/workflows/execute.md`, `distilled/workflows/quick.md`, `distilled/workflows/verify.md`
|
|
491
|
+
- `agents/planner.md`, `agents/executor.md`, `agents/verifier.md`, `distilled/templates/delegates/plan-checker.md`
|
|
492
|
+
- `bin/lib/templates.mjs`, `bin/lib/ui-proof.mjs`, `bin/lib/health.mjs`, `bin/lib/rendering.mjs`
|
|
493
|
+
- `tests/phase.test.cjs`, `tests/gsdd.guards.test.cjs`, `tests/gsdd.health.test.cjs`, `tests/gsdd.init.test.cjs`
|
|
494
|
+
|
|
481
495
|
---
|
|
482
496
|
|
|
483
497
|
## Maintenance
|
|
@@ -25,4 +25,4 @@ Classify each gray area before acting on it:
|
|
|
25
25
|
|
|
26
26
|
Write `{padded_phase}-APPROACH.md` to the phase directory using the approach template.
|
|
27
27
|
|
|
28
|
-
Return structured summary: gray areas explored, decisions captured, assumptions validated/corrected, deferred ideas, path to APPROACH.md.
|
|
28
|
+
Return only a structured summary: gray areas explored, decisions captured, assumptions validated/corrected, deferred ideas, and path to APPROACH.md. Full decision detail belongs in the APPROACH.md artifact, not in the orchestrator context.
|