gsdd-cli 0.19.3 → 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/executor.md +3 -0
- package/agents/planner.md +11 -1
- 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 +1 -1
- 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 +45 -1
- package/distilled/EVIDENCE-INDEX.md +7 -0
- package/distilled/templates/delegates/plan-checker.md +2 -0
- package/distilled/templates/ui-proof.md +174 -0
- package/distilled/workflows/audit-milestone.md +1 -0
- package/distilled/workflows/execute.md +7 -8
- package/distilled/workflows/plan.md +9 -8
- package/distilled/workflows/quick.md +6 -0
- package/distilled/workflows/verify.md +6 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -346,6 +346,7 @@ Workflows are agent skills or commands, not plain shell utilities. How you invok
|
|
|
346
346
|
| `npx -y gsdd-cli init [--tools <platform>]` | Set up `.planning/`, generate skills/adapters |
|
|
347
347
|
| `npx -y gsdd-cli update [--tools <platform>] [--templates]` | Regenerate skills/adapters and refresh the repo-local helper runtime; `--templates` refreshes `.planning/templates/` and role contracts |
|
|
348
348
|
| `npx -y gsdd-cli health [--json]` | Check workspace integrity and generated-surface freshness (healthy/degraded/broken) |
|
|
349
|
+
| `npx -y gsdd-cli ui-proof validate <path> [--claim <public\|publication\|tracked\|delivery\|release>]` | Validate UI proof bundle metadata without requiring browser tooling; use `--claim` only when validating that stronger proof use |
|
|
349
350
|
| `npx -y gsdd-cli file-op <copy\|delete\|regex-sub>` | Run deterministic workspace-confined file copy, delete, and regex substitution |
|
|
350
351
|
| `npx -y gsdd-cli find-phase [N]` | Show phase info as JSON (for agent consumption) |
|
|
351
352
|
| `npx -y gsdd-cli phase-status <N> <status>` | Update a single ROADMAP phase status through the status-aware helper |
|
package/agents/executor.md
CHANGED
|
@@ -209,6 +209,9 @@ Before reporting a task complete:
|
|
|
209
209
|
- if a UI change is involved, verify the relevant rendering path
|
|
210
210
|
- if an API change is involved, hit the endpoint or targeted integration path
|
|
211
211
|
- A task is not complete because code was written. It is complete when the intended verification path actually passes.
|
|
212
|
+
|
|
213
|
+
### UI Proof Execution
|
|
214
|
+
If the plan defines UI proof slots, record observed proof against the exact claim, route/state, observation, evidence kind, artifact path or manual step, privacy metadata, result, and claim limit before claiming task completion. Artifact metadata must include `visibility`, `retention`, `sensitivity`, and `safe_to_publish`; raw screenshots, traces, videos, DOM snapshots, and reports are local-only/unsafe by default and cannot back public, tracked, delivery, release, or publication proof claims. Use `gsdd ui-proof validate <path>` or `gsdd health` when a bundle exists. Artifact count, source comments, AST/cAST findings, semantic search, and Semble-like retrieval are not proof. Missing or weakly linked evidence must be recorded as proof debt, waiver, deferment, or reduced claim language rather than satisfied proof.
|
|
212
215
|
</execution_loop>
|
|
213
216
|
|
|
214
217
|
<checkpoint_protocol>
|
package/agents/planner.md
CHANGED
|
@@ -153,6 +153,14 @@ Any checkpoint must be justified by the task itself, not by planner caution or h
|
|
|
153
153
|
Any plan containing `checkpoint:*` must set `autonomous: false`.
|
|
154
154
|
</task_contract>
|
|
155
155
|
|
|
156
|
+
<ui_proof_planning>
|
|
157
|
+
For UI-sensitive work, plan proof slots that can later be matched exactly to claim, route/state, observation, evidence kind, artifact path or manual step, privacy metadata, result, and claim limit. Use only the stable evidence kinds `code`, `test`, `runtime`, `delivery`, and `human`.
|
|
158
|
+
|
|
159
|
+
Require observed artifacts to carry `visibility`, `retention`, `sensitivity`, and `safe_to_publish`; when a planned slot is meant to support public, publication, tracked, delivery, or release proof, say to validate the observed bundle with `gsdd ui-proof validate <path> --claim <...>`. `gsdd ui-proof validate`/`gsdd health` must catch invalid bundle metadata when present.
|
|
160
|
+
|
|
161
|
+
Do not let source annotations, AST/cAST findings, semantic search, comments, or Semble-like retrieval satisfy proof slots; they are discovery hints only. Human acceptance can narrow or waive a claim and record proof debt, but it must not turn missing or mismatched non-human evidence into `satisfied` proof.
|
|
162
|
+
</ui_proof_planning>
|
|
163
|
+
|
|
156
164
|
<dependency_graph_example>
|
|
157
165
|
Example dependency graph:
|
|
158
166
|
|
|
@@ -184,7 +192,7 @@ Wave rule:
|
|
|
184
192
|
Write one or more `PLAN.md` files to the phase directory.
|
|
185
193
|
|
|
186
194
|
Keep the current GSDD schema exactly:
|
|
187
|
-
- frontmatter keys: `phase`, `plan`, `type`, `wave`, `runtime`, `assurance`, `depends_on`, `files-modified`, `autonomous`, `requirements`, `non_goals`, `hard_boundaries`, `escalation_triggers`, `approval_gates`, `anti_regression_targets`, `known_unknowns`, `high_leverage_surfaces`, `second_pass_required`, `closure_claim_limit`, `parallelism_budget`, `leverage`, `must_haves`
|
|
195
|
+
- frontmatter keys: `phase`, `plan`, `type`, `wave`, `runtime`, `assurance`, `depends_on`, `files-modified`, `autonomous`, `requirements`, `non_goals`, `hard_boundaries`, `escalation_triggers`, `approval_gates`, `anti_regression_targets`, `known_unknowns`, `ui_proof_slots`, `no_ui_proof_rationale`, `high_leverage_surfaces`, `second_pass_required`, `closure_claim_limit`, `parallelism_budget`, `leverage`, `must_haves`
|
|
188
196
|
- typed tasks with `files`, `action`, `verify`, and `done`
|
|
189
197
|
|
|
190
198
|
Typed frontmatter example:
|
|
@@ -215,6 +223,8 @@ anti_regression_targets:
|
|
|
215
223
|
- Existing session middleware behavior remains unchanged for already-supported routes.
|
|
216
224
|
known_unknowns:
|
|
217
225
|
- Exact copy wording for auth errors may still need product confirmation.
|
|
226
|
+
ui_proof_slots: []
|
|
227
|
+
no_ui_proof_rationale: Not UI-sensitive; scoped work does not claim a visible UI outcome.
|
|
218
228
|
high_leverage_surfaces: []
|
|
219
229
|
second_pass_required: false
|
|
220
230
|
closure_claim_limit: Do not claim phase completion until verification satisfies the evidence contract for the scoped truths.
|
package/agents/verifier.md
CHANGED
|
@@ -123,6 +123,8 @@ Do not return a flat symptom list when the same underlying breakage explains mul
|
|
|
123
123
|
|
|
124
124
|
Visual correctness, live interaction quality, and some external integrations still need explicit human checks.
|
|
125
125
|
|
|
126
|
+
For UI proof slots, fail closed unless observed proof is matched to the exact claim, route/state, observation, evidence kind, artifact path or manual step, privacy metadata, result, and claim limit. Artifact metadata must include `visibility`, `retention`, `sensitivity`, and `safe_to_publish`; local-only or unsafe artifacts cannot back public, tracked, delivery, release, or publication proof claims, and `gsdd ui-proof validate`/`gsdd health` metadata failures block the stronger proof claim. Screenshots, traces, reports, Gherkin, a11y scans, E2E outputs, manual notes, source annotations, AST/cAST findings, semantic search, comments, and Semble-like retrieval do not satisfy proof by existence alone. Human acceptance records risk, waiver, deferment, proof debt, or a narrowed claim; it does not upgrade missing or mismatched non-human proof to `satisfied`.
|
|
127
|
+
|
|
126
128
|
## Step 9: Determine overall status
|
|
127
129
|
|
|
128
130
|
- `passed` when all programmatic checks pass and no human-only checks remain
|
package/bin/gsdd.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import { cmdFileOp } from './lib/file-ops.mjs';
|
|
|
19
19
|
import { createCmdHealth } from './lib/health.mjs';
|
|
20
20
|
import { cmdLifecyclePreflight } from './lib/lifecycle-preflight.mjs';
|
|
21
21
|
import { cmdSessionFingerprint } from './lib/session-fingerprint.mjs';
|
|
22
|
+
import { cmdUiProof } from './lib/ui-proof.mjs';
|
|
22
23
|
import { resolveWorkspaceContext } from './lib/workspace-root.mjs';
|
|
23
24
|
|
|
24
25
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -107,6 +108,7 @@ const COMMANDS = {
|
|
|
107
108
|
'file-op': cmdFileOp,
|
|
108
109
|
'lifecycle-preflight': cmdLifecyclePreflight,
|
|
109
110
|
'session-fingerprint': cmdSessionFingerprint,
|
|
111
|
+
'ui-proof': cmdUiProof,
|
|
110
112
|
'find-phase': cmdFindPhase,
|
|
111
113
|
'phase-status': cmdPhaseStatus,
|
|
112
114
|
verify: cmdVerify,
|
|
@@ -132,8 +134,5 @@ async function runCli(cliCommand = command, ...cliArgs) {
|
|
|
132
134
|
await COMMANDS[cliCommand](...normalizedArgs);
|
|
133
135
|
}
|
|
134
136
|
|
|
135
|
-
if (IS_MAIN)
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export { cmdHelp, cmdInit, cmdUpdate, cmdModels, cmdHealth, cmdFileOp, cmdLifecyclePreflight, cmdSessionFingerprint, cmdFindPhase, cmdPhaseStatus, cmdVerify, cmdScaffold, runCli, FRAMEWORK_VERSION, createCliContext };
|
|
137
|
+
if (IS_MAIN) await runCli();
|
|
138
|
+
export { cmdHelp, cmdInit, cmdUpdate, cmdModels, cmdHealth, cmdFileOp, cmdLifecyclePreflight, cmdSessionFingerprint, cmdUiProof, cmdFindPhase, cmdPhaseStatus, cmdVerify, cmdScaffold, runCli, FRAMEWORK_VERSION, createCliContext };
|
package/bin/lib/health.mjs
CHANGED
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
// evaluate once, so CWD must be computed inside function bodies.
|
|
5
5
|
|
|
6
6
|
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
7
|
-
import { join } from 'path';
|
|
7
|
+
import { join, relative } from 'path';
|
|
8
8
|
import { readManifest, detectModifications } from './manifest.mjs';
|
|
9
9
|
import { output } from './cli-utils.mjs';
|
|
10
10
|
import { runTruthChecks, TRUTH_CHECK_IDS } from './health-truth.mjs';
|
|
11
11
|
import { evaluateLifecycleState } from './lifecycle-state.mjs';
|
|
12
12
|
import { evaluateRuntimeFreshness } from './runtime-freshness.mjs';
|
|
13
|
+
import { findUiProofBundleFiles, readUiProofBundleFile, validateUiProofBundle } from './ui-proof.mjs';
|
|
13
14
|
import { resolveWorkspaceContext } from './workspace-root.mjs';
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -31,7 +32,7 @@ export function createCmdHealth(ctx) {
|
|
|
31
32
|
}
|
|
32
33
|
const cwd = workspaceRoot;
|
|
33
34
|
const frameworkSourceMode = isFrameworkSourceRepo(cwd);
|
|
34
|
-
const healthCheckIds = ['E1', 'E2', 'E3', 'E4', 'E5', 'E6', 'E7', 'E8', 'E9', 'W1', 'W2', 'W3', 'W4', 'W5', 'W6', ...TRUTH_CHECK_IDS, 'I1', 'I2', 'I3'];
|
|
35
|
+
const healthCheckIds = ['E1', 'E2', 'E3', 'E4', 'E5', 'E6', 'E7', 'E8', 'E9', 'E10', 'W1', 'W2', 'W3', 'W4', 'W5', 'W6', ...TRUTH_CHECK_IDS, 'I1', 'I2', 'I3'];
|
|
35
36
|
|
|
36
37
|
// Pre-init guard
|
|
37
38
|
if (!existsSync(join(planningDir, 'config.json'))) {
|
|
@@ -123,7 +124,7 @@ export function createCmdHealth(ctx) {
|
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
// E8: critical root template files missing
|
|
126
|
-
const requiredRootFiles = ['spec.md', 'roadmap.md', 'auth-matrix.md'];
|
|
127
|
+
const requiredRootFiles = ['spec.md', 'roadmap.md', 'auth-matrix.md', 'ui-proof.md'];
|
|
127
128
|
const missingRoot = requiredRootFiles.filter((f) => !existsSync(join(templatesDir, f)));
|
|
128
129
|
if (missingRoot.length > 0) {
|
|
129
130
|
errors.push({ id: 'E8', severity: 'ERROR', message: `.planning/templates/ missing critical root files: ${missingRoot.join(', ')}`, fix: 'Run `npx -y gsdd-cli update --templates`' });
|
|
@@ -140,6 +141,23 @@ export function createCmdHealth(ctx) {
|
|
|
140
141
|
}
|
|
141
142
|
}
|
|
142
143
|
|
|
144
|
+
// E10: known UI proof bundles must satisfy deterministic metadata/privacy validation.
|
|
145
|
+
for (const bundlePath of findUiProofBundleFiles(planningDir)) {
|
|
146
|
+
const relativePath = relative(cwd, bundlePath).replace(/\\/g, '/');
|
|
147
|
+
const parsed = readUiProofBundleFile(bundlePath);
|
|
148
|
+
const validation = parsed.errors.length > 0
|
|
149
|
+
? { valid: false, errors: parsed.errors }
|
|
150
|
+
: validateUiProofBundle(parsed.bundle);
|
|
151
|
+
if (!validation.valid) {
|
|
152
|
+
errors.push({
|
|
153
|
+
id: 'E10',
|
|
154
|
+
severity: 'ERROR',
|
|
155
|
+
message: `${relativePath} has invalid UI proof metadata (${validation.errors.map((entry) => entry.code).join(', ')})`,
|
|
156
|
+
fix: 'Run `gsdd ui-proof validate <path>` and add required privacy metadata, claim limits, fixed evidence kinds, observation artifact references, and safe-to-publish handling.',
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
143
161
|
// --- WARNING checks ---
|
|
144
162
|
|
|
145
163
|
// W1: generation-manifest.json missing
|
package/bin/lib/init-runtime.mjs
CHANGED
|
@@ -187,6 +187,8 @@ Commands:
|
|
|
187
187
|
lifecycle-preflight <surface> [phase]
|
|
188
188
|
Inspect deterministic lifecycle gate results for a workflow surface
|
|
189
189
|
session-fingerprint write Rebaseline planning-state drift after reviewing changed planning files
|
|
190
|
+
ui-proof validate <path> [--claim <public|publication|tracked|delivery|release>]
|
|
191
|
+
Validate UI proof metadata; use --claim for stronger proof uses
|
|
190
192
|
help Show this summary
|
|
191
193
|
|
|
192
194
|
Platforms (for --tools):
|
|
@@ -257,6 +259,7 @@ Advanced/internal helpers (kept available, but not the primary first-run user st
|
|
|
257
259
|
lifecycle-preflight Inspect deterministic lifecycle gate results for a workflow surface
|
|
258
260
|
session-fingerprint Rebaseline the local planning-state fingerprint after review
|
|
259
261
|
phase-status Update ROADMAP.md phase status through the local helper surface
|
|
262
|
+
ui-proof Validate UI proof metadata; use --claim for stronger proof uses
|
|
260
263
|
file-op Deterministic workspace-confined file copy/delete/text mutation
|
|
261
264
|
`;
|
|
262
265
|
}
|
|
@@ -6,7 +6,7 @@ const BROWNFIELD_CHANGE_DIR = 'brownfield-change';
|
|
|
6
6
|
const PHASE_LINE_RE = /^\s*[-*]\s*\[([ x-])\]\s*\*\*Phase\s+(\d+(?:\.\d+)*[a-z]?):\s*(.+?)\*\*(?:\s+—\s+\[([^\]]+)])?/i;
|
|
7
7
|
const PHASE_DETAIL_HEADING_RE = /^(#{3,})\s+Phase\s+(\d+(?:\.\d+)*[a-z]?)(?::|\b)/i;
|
|
8
8
|
const PHASE_DETAIL_STATUS_RE = /^\s*\*\*Status\*\*:\s*\[([ x-])\]/i;
|
|
9
|
-
const ACTIVE_MILESTONE_HEADING_RE =
|
|
9
|
+
const ACTIVE_MILESTONE_HEADING_RE = /^#{2,3}\s+(v[^\s]+)\s+(.+)$/im;
|
|
10
10
|
const MILESTONE_LEDGER_HEADING_RE = /^##\s+(?:✅\s+)?(v[^\s]+)\s*(?:—|-)?\s*(.*)$/i;
|
|
11
11
|
const DETAILS_OPEN_RE = /<details\b/i;
|
|
12
12
|
const DETAILS_CLOSE_RE = /<\/details>/i;
|
package/bin/lib/rendering.mjs
CHANGED
|
@@ -13,6 +13,7 @@ const HELPER_LIB_FILES = Object.freeze([
|
|
|
13
13
|
'lifecycle-state.mjs',
|
|
14
14
|
'phase.mjs',
|
|
15
15
|
'session-fingerprint.mjs',
|
|
16
|
+
'ui-proof.mjs',
|
|
16
17
|
'workspace-root.mjs',
|
|
17
18
|
]);
|
|
18
19
|
|
|
@@ -47,6 +48,7 @@ import { cmdFileOp } from './lib/file-ops.mjs';
|
|
|
47
48
|
import { cmdLifecyclePreflight } from './lib/lifecycle-preflight.mjs';
|
|
48
49
|
import { cmdPhaseStatus } from './lib/phase.mjs';
|
|
49
50
|
import { cmdSessionFingerprint } from './lib/session-fingerprint.mjs';
|
|
51
|
+
import { cmdUiProof } from './lib/ui-proof.mjs';
|
|
50
52
|
import { bootstrapHelperWorkspace, consumeWorkspaceRootArg, resolveWorkspaceContext } from './lib/workspace-root.mjs';
|
|
51
53
|
|
|
52
54
|
const COMMANDS = {
|
|
@@ -54,6 +56,7 @@ const COMMANDS = {
|
|
|
54
56
|
'lifecycle-preflight': cmdLifecyclePreflight,
|
|
55
57
|
'phase-status': cmdPhaseStatus,
|
|
56
58
|
'session-fingerprint': cmdSessionFingerprint,
|
|
59
|
+
'ui-proof': cmdUiProof,
|
|
57
60
|
};
|
|
58
61
|
|
|
59
62
|
function printHelp() {
|
|
@@ -71,6 +74,8 @@ function printHelp() {
|
|
|
71
74
|
' Example: node .planning/bin/gsdd.mjs lifecycle-preflight verify 1 --expects-mutation phase-status',
|
|
72
75
|
' session-fingerprint write',
|
|
73
76
|
' Rebaseline planning-state drift after reviewing changed planning files',
|
|
77
|
+
' ui-proof validate <path> [--claim <public|publication|tracked|delivery|release>]',
|
|
78
|
+
' Validate UI proof metadata; use --claim for stronger proof uses',
|
|
74
79
|
'',
|
|
75
80
|
'Advanced option:',
|
|
76
81
|
' --workspace-root <path> Override workspace root discovery before or after the subcommand',
|
package/bin/lib/templates.mjs
CHANGED
|
@@ -23,7 +23,7 @@ export function installProjectTemplates({ planningDir, distilledDir, agentsDir }
|
|
|
23
23
|
console.log(` - WARN: missing expected template subdir: ${subdir}/`);
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
const expectedRootFiles = ['spec.md', 'roadmap.md', 'auth-matrix.md'];
|
|
26
|
+
const expectedRootFiles = ['spec.md', 'roadmap.md', 'auth-matrix.md', 'ui-proof.md'];
|
|
27
27
|
for (const file of expectedRootFiles) {
|
|
28
28
|
if (!existsSync(join(localTemplatesDir, file))) {
|
|
29
29
|
console.log(` - WARN: missing expected root template file: ${file}`);
|
|
@@ -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
|
@@ -72,6 +72,7 @@
|
|
|
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
74
|
61. [Deliberate Subagent Contract](#d61---deliberate-subagent-contract)
|
|
75
|
+
62. [Repo-Native UI Proof Contract](#d62---repo-native-ui-proof-contract)
|
|
75
76
|
|
|
76
77
|
---
|
|
77
78
|
|
|
@@ -956,8 +957,9 @@ Implementation lives under `bin/lib/`:
|
|
|
956
957
|
| E5 | ERROR | `.planning/templates/delegates/` missing or empty |
|
|
957
958
|
| E6 | ERROR | `.planning/templates/research/` missing or empty |
|
|
958
959
|
| E7 | ERROR | `.planning/templates/codebase/` missing or empty |
|
|
959
|
-
| 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`) |
|
|
960
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 |
|
|
961
963
|
| W1 | WARN | `generation-manifest.json` missing |
|
|
962
964
|
| W2 | WARN | Manifest-tracked installed templates/helpers modified locally (hash mismatch vs manifest) |
|
|
963
965
|
| W3 | WARN | Manifest-tracked installed templates/helpers missing from disk but listed in manifest |
|
|
@@ -2813,6 +2815,48 @@ Posture compatibility is part of that closeout contract: `repo_closeout` and `ru
|
|
|
2813
2815
|
|
|
2814
2816
|
---
|
|
2815
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
|
+
|
|
2816
2860
|
## Maintenance
|
|
2817
2861
|
|
|
2818
2862
|
This document is updated when:
|
|
@@ -485,6 +485,13 @@
|
|
|
485
485
|
- GSD comparison source: `get-shit-done/workflows/new-project.md`
|
|
486
486
|
- `tests/gsdd.guards.test.cjs`, `tests/gsdd.invariants.test.cjs`
|
|
487
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
|
+
|
|
488
495
|
---
|
|
489
496
|
|
|
490
497
|
## Maintenance
|
|
@@ -34,6 +34,8 @@ Verify these dimensions:
|
|
|
34
34
|
- `anti_regression_capture`: known prior failures, compatibility risks, and behavior that must not regress are represented in tasks or verification.
|
|
35
35
|
- `escalation_integrity`: tasks include checkpoints or escalation when evidence, permissions, user decisions, or risky ambiguity are required.
|
|
36
36
|
- `closure_honesty`: the plan's done criteria and evidence limits support only claims that execution can actually prove.
|
|
37
|
+
- `closure_honesty`: for UI proof, reject agent-only `looks good` closure, artifact-count proof, unsupported evidence kinds, and human acceptance that converts missing/mismatched non-human evidence into `satisfied` proof. Waiver, deferment, proof debt, or narrowed-claim language is acceptable only when the stronger UI claim is not treated as proven.
|
|
38
|
+
- `closure_honesty`: for UI proof privacy, require artifact `visibility`, `retention`, `sensitivity`, and `safe_to_publish`, require `gsdd ui-proof validate` or `gsdd health` when bundle metadata exists, and reject public/tracked/delivery/publication proof claims backed by local-only or `safe_to_publish: false` artifacts.
|
|
37
39
|
- `high_leverage_review`: high-leverage surfaces have a second-pass review or equivalent contradiction/staleness check before completion.
|
|
38
40
|
- `approach_alignment`: when APPROACH.md is provided, verify that plan tasks implement the chosen approaches from the user's decisions. Check:
|
|
39
41
|
- **Alignment proof valid?** When `workflow.discuss` is `true`, APPROACH.md must record `alignment_status: user_confirmed` or `alignment_status: approved_skip`. Missing alignment proof, unknown status, or agent-discretion-only proof -> `blocker` with `fix_hint` telling the planner to revise APPROACH.md through real user alignment or an explicit user-approved skip.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# UI Proof Bundle Template
|
|
2
|
+
|
|
3
|
+
Use this template when work affects rendered UI or when a plan defines `ui_proof_slots`. Keep the bundle compact, claim-specific, and attached to the relevant phase, quick task, or brownfield change.
|
|
4
|
+
|
|
5
|
+
UI proof uses the existing closure evidence kinds only: `code`, `test`, `runtime`, `delivery`, and `human`. Screenshots, traces, videos, reports, accessibility scans, Gherkin, visual diffs, and manual notes are artifact types or activities that map onto those evidence kinds. They are not new evidence kinds.
|
|
6
|
+
|
|
7
|
+
## Planned Proof Slots
|
|
8
|
+
|
|
9
|
+
Every UI-sensitive plan needs either at least one slot under `ui_proof_slots` or an explicit `no_ui_proof_rationale` explaining why no rendered UI proof is required.
|
|
10
|
+
|
|
11
|
+
```yaml
|
|
12
|
+
ui_proof_slots:
|
|
13
|
+
- slot_id: ui-01
|
|
14
|
+
requirement_id: REQ-01
|
|
15
|
+
claim: "User can complete the changed flow without a broken rendered UI."
|
|
16
|
+
route_state: "/example route, role, data state, and UI state to inspect"
|
|
17
|
+
required_evidence_kinds: [test, runtime]
|
|
18
|
+
optional_evidence_kinds: [human]
|
|
19
|
+
minimum_observations:
|
|
20
|
+
- "Changed control is visible and usable in the stated state."
|
|
21
|
+
- "Expected interaction completes without console/runtime error."
|
|
22
|
+
environment:
|
|
23
|
+
app_url: "http://localhost:3000"
|
|
24
|
+
data_state: "synthetic or seeded data"
|
|
25
|
+
viewport:
|
|
26
|
+
width: 1280
|
|
27
|
+
height: 720
|
|
28
|
+
notes: "Use project default unless responsive behavior is part of the claim."
|
|
29
|
+
manual_acceptance_required: false
|
|
30
|
+
claim_limit: "Does not prove cross-browser layout, full accessibility conformance, production delivery, or unrelated UI states."
|
|
31
|
+
no_ui_proof_rationale: null
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Slot rules:
|
|
35
|
+
- Keep each slot tied to one exact UI claim.
|
|
36
|
+
- Use the lightest proof that can catch a botched rendered experience for that claim.
|
|
37
|
+
- Source annotations, AST/cAST findings, semantic search hits, comments, and Semble-like retrieval may help discover proof obligations. They are discovery hints only; they do not satisfy proof slots.
|
|
38
|
+
- Do not add Playwright, Cypress, Storybook, Cucumber, CI, browser MCP, or visual-regression tooling by default.
|
|
39
|
+
- Human approval is required for visual taste, accessibility judgment, baseline acceptance, subjective polish/layout quality, and privacy publication decisions.
|
|
40
|
+
- Human approval does not replace required non-human evidence when the slot requires `code`, `test`, `runtime`, or `delivery` evidence.
|
|
41
|
+
|
|
42
|
+
## Observed Proof Bundle
|
|
43
|
+
|
|
44
|
+
Create or update this bundle during execution or verification when planned UI proof slots exist. JSON is the canonical machine-readable proof bundle format. Markdown proof files must include fenced JSON for deterministic validation.
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"proof_bundle_version": 1,
|
|
49
|
+
"scope": {
|
|
50
|
+
"work_item": "phase-or-quick-or-brownfield-id",
|
|
51
|
+
"requirement_ids": ["REQ-01"],
|
|
52
|
+
"slot_ids": ["ui-01"],
|
|
53
|
+
"claim": "User can complete the changed flow without a broken rendered UI."
|
|
54
|
+
},
|
|
55
|
+
"route_state": {
|
|
56
|
+
"route": "/example",
|
|
57
|
+
"state": "role, data state, feature flag, loading/error/empty state, or component story"
|
|
58
|
+
},
|
|
59
|
+
"environment": {
|
|
60
|
+
"app_url": "http://localhost:3000",
|
|
61
|
+
"browser": "project default or manual browser",
|
|
62
|
+
"browser_version": "record if known",
|
|
63
|
+
"os": "record if relevant",
|
|
64
|
+
"data_state": "synthetic or seeded data"
|
|
65
|
+
},
|
|
66
|
+
"viewport": {
|
|
67
|
+
"width": 1280,
|
|
68
|
+
"height": 720,
|
|
69
|
+
"device_scale_factor": "record if relevant"
|
|
70
|
+
},
|
|
71
|
+
"evidence_inputs": {
|
|
72
|
+
"kinds": ["test", "runtime"],
|
|
73
|
+
"tools_used": ["manual"]
|
|
74
|
+
},
|
|
75
|
+
"commands_or_manual_steps": [
|
|
76
|
+
{
|
|
77
|
+
"command": "npm run test:e2e -- changed-flow.spec.ts",
|
|
78
|
+
"exit_code": 0,
|
|
79
|
+
"result": "passed",
|
|
80
|
+
"attempts": 1
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"manual_step": "Open /example as synthetic user and complete the changed interaction.",
|
|
84
|
+
"result": "passed"
|
|
85
|
+
}
|
|
86
|
+
],
|
|
87
|
+
"observations": [
|
|
88
|
+
{
|
|
89
|
+
"observation": "Changed control is visible and completes the flow.",
|
|
90
|
+
"claim": "User can complete the changed flow without a broken rendered UI.",
|
|
91
|
+
"route_state": {
|
|
92
|
+
"route": "/example",
|
|
93
|
+
"state": "role, data state, feature flag, loading/error/empty state, or component story"
|
|
94
|
+
},
|
|
95
|
+
"evidence_kind": "runtime",
|
|
96
|
+
"artifact_refs": ["test-results/changed-flow-report/index.html"],
|
|
97
|
+
"privacy": {
|
|
98
|
+
"data_classification": "synthetic",
|
|
99
|
+
"raw_artifacts_safe_to_publish": false,
|
|
100
|
+
"retention": "temporary_review"
|
|
101
|
+
},
|
|
102
|
+
"result": "passed",
|
|
103
|
+
"claim_limit": "Does not prove Safari/WebKit behavior."
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
"artifacts": [
|
|
107
|
+
{
|
|
108
|
+
"path": "test-results/changed-flow-report/index.html",
|
|
109
|
+
"type": "report",
|
|
110
|
+
"visibility": "local_only",
|
|
111
|
+
"retention": "temporary_review",
|
|
112
|
+
"sensitivity": "possible",
|
|
113
|
+
"safe_to_publish": false,
|
|
114
|
+
"notes": "Local report only; not public proof."
|
|
115
|
+
}
|
|
116
|
+
],
|
|
117
|
+
"privacy": {
|
|
118
|
+
"data_classification": "synthetic",
|
|
119
|
+
"redactions": [],
|
|
120
|
+
"raw_artifacts_safe_to_publish": false,
|
|
121
|
+
"retention": "Keep metadata bundle; keep raw artifacts only while needed for review or failed proof triage."
|
|
122
|
+
},
|
|
123
|
+
"manual_acceptance": {
|
|
124
|
+
"required": false,
|
|
125
|
+
"reviewer": null,
|
|
126
|
+
"result": "not_applicable"
|
|
127
|
+
},
|
|
128
|
+
"result": {
|
|
129
|
+
"claim_status": "passed",
|
|
130
|
+
"comparison_status_by_slot": {
|
|
131
|
+
"ui-01": "satisfied"
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
"claim_limits": [
|
|
135
|
+
"Does not prove Safari/WebKit behavior.",
|
|
136
|
+
"Does not prove full WCAG conformance.",
|
|
137
|
+
"Does not prove deployed production behavior."
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Bundle rules:
|
|
143
|
+
- Reference raw screenshots, traces, videos, DOM snapshots, reports, accessibility scans, Gherkin, and visual diffs by path or link. Do not store raw binary or sensitive artifacts inline.
|
|
144
|
+
- Each observation must identify the claim, route/state, evidence kind, artifact references behind it, privacy metadata, result, and claim limit it supports.
|
|
145
|
+
- Every observation `artifact_refs` value must match an `artifacts[].path` or `artifacts[].url` value.
|
|
146
|
+
- Artifact count is never proof. Unsupported or weakly linked artifacts are `partial`, `missing`, `waived`, or `deferred`, not `satisfied`.
|
|
147
|
+
- Each artifact must record the locked privacy fields `visibility`, `retention`, `sensitivity`, and `safe_to_publish`.
|
|
148
|
+
- Raw screenshots, traces, videos, DOM snapshots, and reports default to `visibility: local_only` plus `safe_to_publish: false` unless explicitly classified as sanitized and safe to publish.
|
|
149
|
+
- Local-only or `safe_to_publish: false` artifacts can support local review only; they must not back tracked, public, delivery, release, or publication proof claims.
|
|
150
|
+
- Human acceptance may close a narrowed claim only by recording waiver, deferment, or proof debt; it must not upgrade missing or mismatched non-human proof to `satisfied`.
|
|
151
|
+
- Quick-mode UI proof should use deterministic synthetic IDs such as `quick-001` and `quick-001-ui-01` when roadmap requirement IDs do not exist.
|
|
152
|
+
|
|
153
|
+
## Deterministic Validation
|
|
154
|
+
|
|
155
|
+
Use `gsdd ui-proof validate <path>` on JSON proof-bundle metadata or markdown fenced JSON before relying on a bundle for closure; add `--claim <public|publication|tracked|delivery|release>` only when validating that stronger proof use. Required observed-bundle top-level fields are `proof_bundle_version`, `scope`, `route_state`, `environment`, `viewport`, `evidence_inputs`, `commands_or_manual_steps`, `observations`, `artifacts`, `privacy`, `result`, and `claim_limits`. The validator checks required bundle and observation fields, structured command/manual-step entries, fixed evidence kinds, `result.claim_status`, observation `result`, comparison statuses, non-empty claim limits, locked artifact and observation privacy fields, observation-to-artifact references, workspace-relative/http(s) artifact references, and explicit public/tracked/delivery proof claims that rely on local-only, unsafe, unsanitized, or privacy-contradictory artifacts. `claim_status`, observation `result`, and command/manual-step `result` use `passed`, `failed`, `partial`, `waived`, `deferred`, or `not_applicable`. It is metadata-only and does not inspect raw screenshot, trace, video, DOM, or report contents.
|
|
156
|
+
|
|
157
|
+
## Comparison Statuses
|
|
158
|
+
|
|
159
|
+
Use these statuses when comparing planned slots to observed proof:
|
|
160
|
+
|
|
161
|
+
| Status | Meaning | Claim impact |
|
|
162
|
+
| --- | --- | --- |
|
|
163
|
+
| `satisfied` | Required observations and evidence kinds are present, scoped, and inspectable for the exact claim. | Supports the scoped UI claim. |
|
|
164
|
+
| `partial` | Some proof exists, but observations, artifact references, evidence kinds, privacy metadata, or assurance are weaker than planned. | Record a reduced claim or gap. |
|
|
165
|
+
| `missing` | Required proof is absent. | Blocks the UI claim unless explicitly waived or deferred. |
|
|
166
|
+
| `waived` | A human or approved plan waiver accepts the risk. | Does not prove the UI claim. |
|
|
167
|
+
| `deferred` | Proof moved to later work. | Current work must not claim the UI behavior is proven. |
|
|
168
|
+
| `not_applicable` | Accepted rationale says no UI proof is required. | No UI proof gap for that claim. |
|
|
169
|
+
|
|
170
|
+
Proof debt notes should name the slot, claim, route/state, missing or weak linkage, human acceptance basis, narrowed claim limit, and follow-up trigger.
|
|
171
|
+
|
|
172
|
+
## Claim Boundary
|
|
173
|
+
|
|
174
|
+
A UI proof bundle proves only the scoped claim, route/state, environment, viewport, observations, and evidence kinds it records. It does not imply broad visual quality, cross-browser coverage, full accessibility conformance, production delivery, release readiness, or public proof unless those dimensions are explicitly planned, evidenced, and classified safe to publish.
|
|
@@ -126,6 +126,7 @@ Combine:
|
|
|
126
126
|
- Integration checker's report (wiring gaps, auth gaps, broken flows, requirements integration map)
|
|
127
127
|
- Evidence observations by kind (`code`, `test`, `runtime`, `delivery`, `human`) from phase verifications, summaries, integration findings, and delivery metadata
|
|
128
128
|
- Release claim posture observations: selected `release_claim_posture`, unsupported claims, waivers, deferrals, and contradiction checks for public, runtime, delivery, planning-drift, and generated-surface claims
|
|
129
|
+
- UI proof debt from phase/quick proof bundles or verification gaps, preserving the rule that waiver/deferment/human acceptance narrows claims rather than satisfying missing proof
|
|
129
130
|
|
|
130
131
|
## 5. 3-Source Cross-Reference
|
|
131
132
|
|
|
@@ -164,6 +164,11 @@ Before reporting a task complete:
|
|
|
164
164
|
- if an API change is involved, hit the endpoint or targeted integration path
|
|
165
165
|
- A task is not complete because code was written. It is complete when the intended verification path actually passes.
|
|
166
166
|
|
|
167
|
+
### UI Proof Execution
|
|
168
|
+
If the plan defines non-empty `ui_proof_slots`, create or update the observed UI proof bundle before claiming completion; required top-level fields are `proof_bundle_version`, `scope`, `route_state`, `environment`, `viewport`, `evidence_inputs`, `commands_or_manual_steps`, `observations`, `artifacts`, `privacy`, `result`, and `claim_limits`.
|
|
169
|
+
Use existing UI tooling when available and cheap; manual/browser proof is acceptable when it records route/state, steps, observations, artifact references, and claim limits. Do not install Playwright, Cypress, Cucumber, Storybook, browser MCP, CI, or visual-regression tooling by default. Screenshots, traces, videos, reports, accessibility scans, Gherkin, visual diffs, and manual notes map onto existing evidence kinds, not new evidence kinds; reference raw artifacts by path/link instead of storing them inline.
|
|
170
|
+
Each artifact entry must include `visibility`, `retention`, `sensitivity`, and `safe_to_publish`; raw screenshots, traces, videos, DOM snapshots, and reports default to `local_only` and `safe_to_publish: false` unless explicitly sanitized. Use `gsdd ui-proof validate <path>` when bundle metadata exists, adding `--claim <...>` only when relying on the bundle for public, tracked, delivery, release, or publication proof. Visual taste, accessibility judgment, baseline acceptance, subjective polish/layout quality, and privacy publication decisions require human evidence or explicit waiver; artifact count, source comments, AST/cAST findings, semantic search, and Semble-like retrieval are not proof. If evidence does not match the slot claim, route/state, observation, artifact path/manual step, privacy metadata, result, and claim limit, record proof debt, waiver, deferment, or reduced claim language rather than `satisfied` proof.
|
|
171
|
+
|
|
167
172
|
### Git Guidance
|
|
168
173
|
|
|
169
174
|
```bash
|
|
@@ -440,20 +445,14 @@ Execution is done when all of these are true:
|
|
|
440
445
|
</success_criteria>
|
|
441
446
|
|
|
442
447
|
<completion>
|
|
443
|
-
Report
|
|
444
|
-
|
|
448
|
+
Report what was accomplished, then present the next step:
|
|
445
449
|
---
|
|
446
450
|
**Completed:** Plan execution — created `.planning/phases/{phase_dir}/{plan_id}-SUMMARY.md`.
|
|
447
|
-
|
|
448
451
|
**Next step:** Check `.planning/config.json` → `workflow.verifier`:
|
|
449
452
|
- If `true`: run `/gsdd-verify` — verify that the phase goal was achieved
|
|
450
453
|
- If `false` (or key missing): run `/gsdd-progress` — check status and route to the next phase
|
|
451
454
|
|
|
452
|
-
Also available:
|
|
453
|
-
- `/gsdd-plan` — plan the next wave (if more plans remain in this phase)
|
|
454
|
-
- `/gsdd-quick` — handle a sub-hour task outside the phase cycle
|
|
455
|
-
- `/gsdd-pause` — save context for later if stopping work
|
|
456
|
-
|
|
455
|
+
Also available: `/gsdd-plan` for the next wave, `/gsdd-quick` for sub-hour work, or `/gsdd-pause` to save context.
|
|
457
456
|
Consider clearing context before starting the next workflow for best results.
|
|
458
457
|
---
|
|
459
458
|
</completion>
|
|
@@ -135,6 +135,10 @@ Also verify milestone truth is not self-contradictory across the planning surfac
|
|
|
135
135
|
If any of these are missing or contradictory, STOP. Report the exact missing contract field or contradiction. Do not improvise a stronger phase contract from chat context alone.
|
|
136
136
|
</phase_contract_gate>
|
|
137
137
|
|
|
138
|
+
<ui_proof_planning>
|
|
139
|
+
For UI-sensitive work, include compact `ui_proof_slots` with `slot_id`, optional `requirement_id`, `claim`, `route_state`, fixed evidence kinds (`code`, `test`, `runtime`, `delivery`, `human`), `minimum_observations`, `environment`, `viewport`, `manual_acceptance_required`, and `claim_limit`; otherwise set `no_ui_proof_rationale`.
|
|
140
|
+
Do not create slots for backend-only, CLI-only, docs-only, or refactor-only work unless the plan claims a visible UI outcome. Evidence must later match claim, route/state, observation, artifact path, evidence kind, privacy metadata, result, and claim limit; local-only or unsafe artifacts cannot support public, publication, tracked, delivery, or release proof claims. Human approval does not replace required `code`, `test`, `runtime`, or `delivery` evidence.
|
|
141
|
+
</ui_proof_planning>
|
|
138
142
|
<goal_backward_planning>
|
|
139
143
|
Plan backward from success criteria.
|
|
140
144
|
|
|
@@ -192,6 +196,8 @@ anti_regression_targets:
|
|
|
192
196
|
- Existing session middleware behavior remains unchanged for already-supported routes.
|
|
193
197
|
known_unknowns:
|
|
194
198
|
- Exact copy wording for auth errors may still need product confirmation.
|
|
199
|
+
ui_proof_slots: []
|
|
200
|
+
no_ui_proof_rationale: Not UI-sensitive; scoped work does not claim a visible UI outcome.
|
|
195
201
|
high_leverage_surfaces: []
|
|
196
202
|
second_pass_required: false
|
|
197
203
|
closure_claim_limit: Do not claim phase completion until verification satisfies the evidence contract for the scoped truths.
|
|
@@ -221,6 +227,7 @@ Schema rules:
|
|
|
221
227
|
- `files-modified` should list the files this plan is expected to touch
|
|
222
228
|
- `must_haves` must trace back to roadmap success criteria
|
|
223
229
|
- `non_goals`, `hard_boundaries`, `escalation_triggers`, and `closure_claim_limit` must not be empty
|
|
230
|
+
- include `ui_proof_slots` for UI-sensitive work or `no_ui_proof_rationale` otherwise
|
|
224
231
|
- `leverage.lost`, `leverage.kept`, and `leverage.gained` must all be explicit
|
|
225
232
|
- `second_pass_required: true` if `high_leverage_surfaces` is non-empty
|
|
226
233
|
- `parallelism_budget.max_concurrent_plans` must stay `1` unless the plan proves disjoint write ownership
|
|
@@ -607,7 +614,7 @@ Planning is done when all of these are true:
|
|
|
607
614
|
- [ ] Plan self-check passed
|
|
608
615
|
- [ ] Success criteria from `ROADMAP.md` are represented as must-haves
|
|
609
616
|
- [ ] Goal-backward derivation from criteria to artifacts to key links to tasks is explicit
|
|
610
|
-
- [ ] Every plan has frontmatter with `phase`, `plan`, `type`, `wave`, `depends_on`, `files-modified`, `autonomous`, `requirements`, `non_goals`, `hard_boundaries`, `escalation_triggers`, `approval_gates`, `anti_regression_targets`, `closure_claim_limit`, `parallelism_budget`, `leverage`, and `must_haves`
|
|
617
|
+
- [ ] Every plan has frontmatter with `phase`, `plan`, `type`, `wave`, `depends_on`, `files-modified`, `autonomous`, `requirements`, `non_goals`, `hard_boundaries`, `escalation_triggers`, `approval_gates`, `anti_regression_targets`, `ui_proof_slots` or `no_ui_proof_rationale`, `closure_claim_limit`, `parallelism_budget`, `leverage`, and `must_haves`
|
|
611
618
|
- [ ] Every plan frontmatter records `runtime` and `assurance`
|
|
612
619
|
- [ ] Every plan records checker outcome in a structured `<checks>` block
|
|
613
620
|
- [ ] Every task has XML structure with `id`, `type`, `files`, `action`, `verify`, and `done`
|
|
@@ -620,18 +627,12 @@ Planning is done when all of these are true:
|
|
|
620
627
|
|
|
621
628
|
<completion>
|
|
622
629
|
Report to the user what was accomplished, then present the next step:
|
|
623
|
-
|
|
624
630
|
---
|
|
625
631
|
**Completed:** Phase planning — created `.planning/phases/{phase_dir}/{plan_id}-PLAN.md`.
|
|
626
632
|
**Planning stops here:** `gsdd-plan` ends after the plan artifact is written. Do not start implementation in this same run, and do not treat imperative handoff text as execution authorization.
|
|
627
633
|
Installed generated runtime surfaces are trusted through rendering, not reviewer memory: `npx -y gsdd-cli health` compares any local generated skill/adapter surfaces against current render output, and `npx -y gsdd-cli update` regenerates them when they drift. Bare `gsdd health` / `gsdd update` are equivalent only when globally installed.
|
|
628
|
-
|
|
629
634
|
**Next workflow:** `/gsdd-execute` — start execution in a separate run when the user explicitly wants implementation to begin
|
|
630
|
-
|
|
631
|
-
Also available:
|
|
632
|
-
- `/gsdd-plan` — create additional plans for the same phase (if multi-wave)
|
|
633
|
-
- `/gsdd-progress` — check overall project status
|
|
634
|
-
|
|
635
|
+
Also available: `/gsdd-plan` for another wave, or `/gsdd-progress` for overall status.
|
|
635
636
|
Consider clearing context before starting the next workflow for best results.
|
|
636
637
|
---
|
|
637
638
|
</completion>
|
|
@@ -118,6 +118,10 @@ Delegate to the planner role in quick mode.
|
|
|
118
118
|
- No research phase, no ROADMAP requirements
|
|
119
119
|
- Do NOT extract phase requirement IDs — there is no active phase
|
|
120
120
|
- Derive must-haves directly from the task description
|
|
121
|
+
- If the quick task is UI-sensitive, include proportional `ui_proof_slots` with slot_id, claim, route_state, required_evidence_kinds, minimum_observations, environment, viewport, manual_acceptance_required, and claim_limit; otherwise include a short `no_ui_proof_rationale`
|
|
122
|
+
- UI proof slots must be matchable to exact observed evidence later: claim, route/state, observation, evidence kind, artifact path or manual step, privacy metadata, result, and claim limit. Discovery hints from source comments, AST/cAST, semantic search, or Semble-like retrieval do not satisfy proof.
|
|
123
|
+
- Observed artifact metadata must include `visibility`, `retention`, `sensitivity`, and `safe_to_publish`; raw screenshots, traces, videos, DOM snapshots, and reports are local-only/unsafe by default. Use `gsdd ui-proof validate <path>` or `gsdd health` when a bundle exists; add `--claim <...>` only for public, publication, tracked, delivery, or release proof use.
|
|
124
|
+
- Keep UI proof proportional: do not scaffold Playwright, Cypress, Cucumber, Storybook, CI, browser MCP, or visual-regression tooling by default
|
|
121
125
|
- Ignore <planning_process> Step 1 requirement extraction; use inline goal-backward planning only
|
|
122
126
|
- Target minimal context usage
|
|
123
127
|
|
|
@@ -263,6 +267,8 @@ Delegate to the executor role.
|
|
|
263
267
|
- Skip the <state_updates> section of your role contract entirely
|
|
264
268
|
- Do NOT update ROADMAP.md phase status or SPEC.md current state
|
|
265
269
|
- Create summary at: `.planning/quick/$NEXT_NUM-$SLUG/$NEXT_NUM-SUMMARY.md`
|
|
270
|
+
- If the quick plan defines `ui_proof_slots`, create or update `.planning/quick/$NEXT_NUM-$SLUG/UI-PROOF.md` with fenced JSON containing required top-level fields: `proof_bundle_version`, `scope`, `route_state`, `environment`, `viewport`, `evidence_inputs`, `commands_or_manual_steps`, `observations`, `artifacts`, `privacy`, `result`, and `claim_limits`
|
|
271
|
+
- Human approval for visual taste, accessibility judgment, baseline acceptance, subjective polish/layout quality, or privacy publication does not replace required `code`, `test`, `runtime`, or `delivery` evidence
|
|
266
272
|
|
|
267
273
|
**Output:** `.planning/quick/$NEXT_NUM-$SLUG/$NEXT_NUM-SUMMARY.md`
|
|
268
274
|
**Return:** Summary file path and completion status.
|
|
@@ -128,6 +128,12 @@ Rules:
|
|
|
128
128
|
Note: this step does NOT replace levels 1–3. An artifact can satisfy the evidence-kind requirement and still fail Level 2 (substantive) or Level 3 (wired). Both checks must run.
|
|
129
129
|
</evidence_contract>
|
|
130
130
|
|
|
131
|
+
<ui_proof_comparison>
|
|
132
|
+
If the plan defines non-empty `ui_proof_slots`, compare planned UI proof against observed bundles before closure. If the plan records only `no_ui_proof_rationale`, verify the rationale instead of requiring a bundle. Each observed bundle must include top-level `proof_bundle_version`, `scope`, `route_state`, `environment`, `viewport`, `evidence_inputs`, `commands_or_manual_steps`, `observations`, `artifacts`, `privacy`, `result`, and `claim_limits`.
|
|
133
|
+
Classify each slot as exactly one of: `satisfied`, `partial`, `missing`, `waived`, `deferred`, or `not_applicable`. Waiver/deferment narrows the claim; it is not proof. Screenshots, traces, videos, reports, accessibility scans, Gherkin, visual diffs, and manual notes are artifact types or activities mapped onto existing evidence kinds, not new evidence kinds. Artifact count is never proof; each artifact must tie to the slot claim, route/state, observation, artifact path/link, privacy metadata, and claim limit.
|
|
134
|
+
Artifact privacy metadata must include `visibility`, `retention`, `sensitivity`, and `safe_to_publish`; raw screenshots, traces, videos, DOM snapshots, and reports default to local-only and unsafe unless sanitized. Run `gsdd ui-proof validate <path>` or treat `gsdd health` E10 as blocking; add `--claim <...>` when relying on the bundle for public, tracked, delivery, release, or publication proof. Visual taste, accessibility judgment, baseline acceptance, subjective polish/layout quality, and privacy publication require human evidence or explicit waiver; human approval does not replace required `code`, `test`, `runtime`, or `delivery` evidence. Source annotations, AST/cAST findings, semantic search, comments, and Semble-like retrieval are discovery hints only.
|
|
135
|
+
</ui_proof_comparison>
|
|
136
|
+
|
|
131
137
|
<verification_levels>
|
|
132
138
|
Check every artifact at three levels. A common failure mode is a file that exists but is still a stub.
|
|
133
139
|
### Level 1: Exists
|
|
@@ -423,13 +429,9 @@ Verification is done when all of these are true:
|
|
|
423
429
|
Report the verification result to the user, then present the next step:
|
|
424
430
|
|
|
425
431
|
---
|
|
426
|
-
|
|
427
432
|
**Completed:** Phase verification — created `.planning/phases/{phase_dir}/{phase_num}-VERIFICATION.md`.
|
|
428
|
-
|
|
429
433
|
If status is `passed`: **Next step:** `/gsdd-progress` — route to the next phase or milestone audit.
|
|
430
434
|
If status is `gaps_found`: **Next step:** `/gsdd-plan` — re-plan to close the identified gaps.
|
|
431
435
|
If status is `human_needed`: **Next step:** `/gsdd-verify-work`, then rerun `/gsdd-verify` with UAT results.
|
|
432
|
-
|
|
433
436
|
Consider clearing context before starting the next workflow for best results.
|
|
434
|
-
|
|
435
437
|
</completion>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gsdd-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Workspine — a repo-native delivery spine for long-horizon AI-assisted work, with directly validated support for Claude Code, Codex CLI, and OpenCode, published as gsdd-cli.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|