rcf-lite 0.25.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/blueprints/application-account-settings/docs/topics.md +1 -1
- package/blueprints/application-admin-console/docs/topics.md +1 -1
- package/blueprints/application-api-rest/docs/topics.md +1 -1
- package/blueprints/application-charts/docs/topics.md +1 -1
- package/blueprints/application-dashboard/docs/topics.md +1 -1
- package/blueprints/application-datatable/docs/topics.md +1 -1
- package/blueprints/application-empty-error-states/docs/topics.md +1 -1
- package/blueprints/application-error-handling/docs/topics.md +1 -1
- package/blueprints/application-file-upload/docs/topics.md +1 -1
- package/blueprints/application-forms-wizard/docs/topics.md +1 -1
- package/blueprints/application-notifications-in-app/docs/topics.md +1 -1
- package/blueprints/application-onboarding-tour/docs/topics.md +1 -1
- package/blueprints/application-spa/docs/topics.md +1 -1
- package/blueprints/delivery-ci-workflows/docs/topics.md +1 -1
- package/blueprints/deploy-cloudflare-workers/docs/topics.md +1 -1
- package/blueprints/deploy-hetzner-server/CHANGELOG.md +21 -0
- package/blueprints/deploy-hetzner-server/blueprint.json +1 -1
- package/blueprints/deploy-hetzner-server/contributions/probes/cloud-init-render-lint.mjs +12 -5
- package/blueprints/deploy-hetzner-server/contributions/probes/hcloud-dry-run-mock.mjs +68 -20
- package/blueprints/deploy-hetzner-server/contributions/probes/manifest-schema-validate.mjs +8 -6
- package/blueprints/deploy-hetzner-server/contributions/probes/probe-utils.mjs +7 -1
- package/blueprints/deploy-hetzner-server/contributions/probes/real-account-cloud-init-hardened.mjs +2 -1
- package/blueprints/deploy-hetzner-server/contributions/templates/cloud-init.yaml.tmpl +26 -6
- package/blueprints/deploy-hetzner-server/docs/topics.md +1 -1
- package/blueprints/edge-cloudflare-access/docs/topics.md +1 -1
- package/blueprints/edge-cloudflare-rate-limiting/docs/topics.md +1 -1
- package/blueprints/edge-cloudflare-tunnel/CHANGELOG.md +10 -0
- package/blueprints/edge-cloudflare-tunnel/blueprint.json +1 -1
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/manifest-schema-validate.mjs +4 -1
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/real-account-connector-healthy.mjs +110 -21
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/real-account-tunnel-hostname-routes.mjs +123 -30
- package/blueprints/edge-cloudflare-tunnel/docs/topics.md +1 -1
- package/blueprints/edge-cloudflare-turnstile/docs/topics.md +1 -1
- package/blueprints/email-smtp-resend/docs/topics.md +1 -1
- package/blueprints/jobs-background/docs/topics.md +1 -1
- package/blueprints/messaging-queue-cloudflare/CHANGELOG.md +19 -0
- package/blueprints/messaging-queue-cloudflare/README.md +2 -2
- package/blueprints/messaging-queue-cloudflare/blueprint.json +1 -1
- package/blueprints/messaging-queue-cloudflare/contributions/probes/real-account-concurrency-smoke.mjs +192 -37
- package/blueprints/messaging-queue-cloudflare/docs/topics.md +1 -1
- package/blueprints/object-storage-s3/CHANGELOG.md +11 -0
- package/blueprints/object-storage-s3/README.md +14 -1
- package/blueprints/object-storage-s3/blueprint.json +12 -1
- package/blueprints/object-storage-s3/contributions/adrs/adr-2905-object-storage-s3-hetzner-object-storage-provider.json +30 -0
- package/blueprints/object-storage-s3/contributions/probes/hetzner-object-storage-round-trip.mjs +136 -0
- package/blueprints/object-storage-s3/contributions/probes/run-hetzner-object-storage-round-trip.mjs +11 -0
- package/blueprints/object-storage-s3/contributions/requirements/object-storage-s3-req-101.json +18 -0
- package/blueprints/object-storage-s3/contributions/tacs/tac-2904-object-storage-s3-hetzner-endpoint-helper.json +34 -0
- package/blueprints/object-storage-s3/contributions/user-stories/object-storage-s3-us-28110.json +25 -0
- package/blueprints/object-storage-s3/docs/topics.md +1 -1
- package/blueprints/object-storage-s3/guide/object-storage-s3.md +36 -0
- package/blueprints/observability-essentials/docs/topics.md +1 -1
- package/blueprints/observability-logging/docs/topics.md +1 -1
- package/blueprints/observability-probe-endpoints/docs/topics.md +1 -1
- package/blueprints/persistence-data-d1/docs/topics.md +1 -1
- package/blueprints/persistence-data-postgres/docs/topics.md +1 -1
- package/blueprints/persistence-data-sqlite/docs/topics.md +1 -1
- package/blueprints/platform-cloudflare-cron-triggers/docs/topics.md +1 -1
- package/blueprints/platform-cloudflare-durable-objects/CHANGELOG.md +15 -0
- package/blueprints/platform-cloudflare-durable-objects/blueprint.json +1 -1
- package/blueprints/platform-cloudflare-durable-objects/contributions/probes/namespace-facade-ready.mjs +3 -3
- package/blueprints/platform-cloudflare-durable-objects/contributions/probes/probe-utils.mjs +28 -14
- package/blueprints/platform-cloudflare-durable-objects/contributions/probes/real-account-storage-smoke.mjs +132 -26
- package/blueprints/platform-cloudflare-durable-objects/contributions/probes/sole-reader-scan.mjs +40 -39
- package/blueprints/platform-cloudflare-durable-objects/contributions/probes/storage-round-trip.mjs +26 -16
- package/blueprints/platform-cloudflare-durable-objects/contributions/probes/websocket-hub-broadcast.mjs +31 -34
- package/blueprints/platform-cloudflare-durable-objects/docs/topics.md +1 -1
- package/blueprints/platform-cloudflare-kv/CHANGELOG.md +40 -1
- package/blueprints/platform-cloudflare-kv/README.md +41 -13
- package/blueprints/platform-cloudflare-kv/blueprint.json +1 -1
- package/blueprints/platform-cloudflare-kv/contributions/probes/cache-aside-hit-then-miss.mjs +26 -26
- package/blueprints/platform-cloudflare-kv/contributions/probes/event-secrecy.mjs +49 -31
- package/blueprints/platform-cloudflare-kv/contributions/probes/facade-round-trip.mjs +58 -16
- package/blueprints/platform-cloudflare-kv/contributions/probes/list-with-prefix.mjs +6 -5
- package/blueprints/platform-cloudflare-kv/contributions/probes/real-account-eventual-consistency-smoke.mjs +131 -67
- package/blueprints/platform-cloudflare-kv/docs/topics.md +1 -1
- package/blueprints/platform-docker-compose-host/docs/topics.md +1 -1
- package/blueprints/security-auth-clerk/docs/topics.md +1 -1
- package/blueprints/security-auth-keycloak/docs/topics.md +1 -1
- package/blueprints/security-auth-magic-link/docs/topics.md +1 -1
- package/blueprints/security-auth-oauth2/docs/topics.md +1 -1
- package/blueprints/security-secrets-management/docs/topics.md +1 -1
- package/fixtures/canary-manifest.json +9 -9
- package/guidance/harness-template.md +67 -0
- package/guidance/managed/agent-instructions-block.hash +1 -1
- package/guidance/managed/agent-instructions-block.md +67 -0
- package/package.json +2 -2
- package/rcf/code-nodes/cn-500.json +12 -0
- package/rcf/code-nodes/cn-510.json +12 -0
- package/rcf/code-nodes/cn-520.json +14 -0
- package/rcf/code-nodes/cn-521.json +12 -0
- package/rcf/code-nodes/cn-522.json +13 -0
- package/rcf/code-nodes/cn-523.json +12 -0
- package/rcf/code-nodes/cn-524.json +14 -0
- package/rcf/code-nodes/cn-525.json +12 -0
- package/rcf/code-nodes/cn-526.json +14 -0
- package/rcf/fbs/fbs-160.json +15 -0
- package/rcf/fbs/fbs-165.json +15 -0
- package/rcf/fbs/fbs-170.json +16 -0
- package/rcf/fbs/fbs-171.json +16 -0
- package/rcf/fbs/fbs-172.json +15 -0
- package/rcf/fbs/fbs-173.json +17 -0
- package/rcf/fbs/fbs-174.json +17 -0
- package/rcf/requirements/req-140.json +149 -0
- package/rcf/requirements/req-145.json +53 -0
- package/rcf/requirements/req-150.json +20 -0
- package/rcf/requirements/req-151.json +85 -0
- package/rcf/requirements/req-152.json +20 -0
- package/rcf/requirements/req-153.json +59 -0
- package/rcf/requirements/req-154.json +44 -0
- package/rcf/test-suites/ts-071.json +2 -2
- package/rcf/test-suites/ts-086.json +2 -2
- package/rcf/test-suites/ts-170.json +22 -0
- package/rcf/test-suites/ts-175.json +22 -0
- package/rcf/test-suites/ts-180.json +30 -0
- package/rcf/test-suites/ts-181.json +30 -0
- package/rcf/test-suites/ts-182.json +22 -0
- package/rcf/test-suites/ts-183.json +38 -0
- package/rcf/test-suites/ts-184.json +38 -0
- package/rcf/user-stories/us-14001.json +21 -0
- package/rcf/user-stories/us-14501.json +24 -0
- package/rcf/user-stories/us-15001.json +26 -0
- package/rcf/user-stories/us-15101.json +26 -0
- package/rcf/user-stories/us-15201.json +21 -0
- package/rcf/user-stories/us-15301.json +31 -0
- package/rcf/user-stories/us-15401.json +31 -0
- package/releases/releases.yaml +11 -1
- package/src/blueprint/apply.js +64 -0
- package/src/blueprint/consistency-lint.js +432 -0
- package/src/blueprint/disposition-ledger.js +224 -0
- package/src/blueprint/dispositions.js +44 -0
- package/src/blueprint/index.js +19 -0
- package/src/browser-verify/manifest-writer.js +53 -2
- package/src/browser-verify/pack-runner.js +18 -0
- package/src/cli/blueprint.js +103 -0
- package/src/setup/identity-seed.js +18 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// Per-slug disposition ledger for applied blueprints. Sits alongside
|
|
2
|
+
// the existing `<slug>.applied.json` sidecar (see capabilities.js) and
|
|
3
|
+
// records one entry per AC contributed by the blueprint, plus the
|
|
4
|
+
// disposition the applying agent (or the operator) recorded.
|
|
5
|
+
//
|
|
6
|
+
// Schema follow-up: a `$defs/blueprintDispositionLedger` shape lands
|
|
7
|
+
// on `@stravica-ai/rcf-schemas` alongside the four fields the 0.6.2
|
|
8
|
+
// bump already ships (deliveredBy, ownerRef, disposition,
|
|
9
|
+
// vendorCitation). Until it does, the ledger validates itself via the
|
|
10
|
+
// local `validateRecord` guard so a malformed write refuses before
|
|
11
|
+
// touching the file.
|
|
12
|
+
//
|
|
13
|
+
// Ledger location: `rcf/blueprints/<slug>.disposition.json`.
|
|
14
|
+
|
|
15
|
+
import { readFile, mkdir, writeFile, stat } from 'node:fs/promises';
|
|
16
|
+
import { join, dirname } from 'node:path';
|
|
17
|
+
|
|
18
|
+
const ACTIONS = new Set([
|
|
19
|
+
'pending-disposition',
|
|
20
|
+
'pending-operator',
|
|
21
|
+
'accepted',
|
|
22
|
+
'adjusted',
|
|
23
|
+
'dropped',
|
|
24
|
+
'escalated-as-defect',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {object} DispositionRecord
|
|
29
|
+
* @property {string} acId
|
|
30
|
+
* @property {string} storyId
|
|
31
|
+
* @property {'fixed'|'template'} [sourceDisposition]
|
|
32
|
+
* @property {'pending-disposition'|'pending-operator'|'accepted'|'adjusted'|'dropped'|'escalated-as-defect'} action
|
|
33
|
+
* @property {string} [reason]
|
|
34
|
+
* @property {string} [resolvedAt]
|
|
35
|
+
* @property {string} [resolvedBy]
|
|
36
|
+
* @property {string} [escalatedAt]
|
|
37
|
+
* @property {boolean} [escalatedToOperator]
|
|
38
|
+
* @property {string} [defectUrl]
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Ledger file path (relative to project root).
|
|
43
|
+
*
|
|
44
|
+
* @param {string} slug
|
|
45
|
+
*/
|
|
46
|
+
export function ledgerRelPath(slug) {
|
|
47
|
+
return join('rcf', 'blueprints', `${slug}.disposition.json`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Absolute path helper.
|
|
52
|
+
*/
|
|
53
|
+
export function ledgerAbsPath(projectRoot, slug) {
|
|
54
|
+
return join(projectRoot, ledgerRelPath(slug));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Best-effort shape check for a single ledger record. Returns null on
|
|
59
|
+
* ok, an error string on any refusal.
|
|
60
|
+
*
|
|
61
|
+
* @param {any} rec
|
|
62
|
+
* @returns {string | null}
|
|
63
|
+
*/
|
|
64
|
+
function validateRecord(rec) {
|
|
65
|
+
if (!rec || typeof rec !== 'object') return 'record is not an object';
|
|
66
|
+
if (typeof rec.acId !== 'string' || rec.acId.length === 0) return 'acId is missing';
|
|
67
|
+
if (typeof rec.storyId !== 'string' || rec.storyId.length === 0) return 'storyId is missing';
|
|
68
|
+
if (typeof rec.action !== 'string' || !ACTIONS.has(rec.action)) return `action must be one of ${[...ACTIONS].join(', ')}; got '${rec.action}'`;
|
|
69
|
+
if (rec.sourceDisposition !== undefined && rec.sourceDisposition !== 'fixed' && rec.sourceDisposition !== 'template') {
|
|
70
|
+
return `sourceDisposition must be 'fixed' or 'template' when set; got '${rec.sourceDisposition}'`;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Read a slug's ledger from disk. Missing file returns null.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} projectRoot
|
|
79
|
+
* @param {string} slug
|
|
80
|
+
* @returns {Promise<{ slug: string, schemaVersion: number, records: DispositionRecord[] } | null>}
|
|
81
|
+
*/
|
|
82
|
+
export async function readLedger(projectRoot, slug) {
|
|
83
|
+
const abs = ledgerAbsPath(projectRoot, slug);
|
|
84
|
+
try {
|
|
85
|
+
const raw = await readFile(abs, 'utf8');
|
|
86
|
+
return JSON.parse(raw);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
if (err && err.code === 'ENOENT') return null;
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Presence check for the ledger file. Does not parse.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} projectRoot
|
|
97
|
+
* @param {string} slug
|
|
98
|
+
* @returns {Promise<boolean>}
|
|
99
|
+
*/
|
|
100
|
+
export async function ledgerExists(projectRoot, slug) {
|
|
101
|
+
try { await stat(ledgerAbsPath(projectRoot, slug)); return true; } catch { return false; }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Write a whole ledger document (overwrites). Validates every record
|
|
106
|
+
* before touching disk; a bad record returns { error: string } and
|
|
107
|
+
* writes nothing.
|
|
108
|
+
*
|
|
109
|
+
* @param {object} args
|
|
110
|
+
* @param {string} args.projectRoot
|
|
111
|
+
* @param {string} args.slug
|
|
112
|
+
* @param {DispositionRecord[]} args.records
|
|
113
|
+
* @returns {Promise<{ path: string } | { error: string }>}
|
|
114
|
+
*/
|
|
115
|
+
export async function writeLedger({ projectRoot, slug, records }) {
|
|
116
|
+
if (!Array.isArray(records)) return { error: 'records must be an array' };
|
|
117
|
+
for (const r of records) {
|
|
118
|
+
const err = validateRecord(r);
|
|
119
|
+
if (err) return { error: `invalid ledger record for AC '${r?.acId ?? '?'}': ${err}` };
|
|
120
|
+
}
|
|
121
|
+
const abs = ledgerAbsPath(projectRoot, slug);
|
|
122
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
123
|
+
const payload = { slug, schemaVersion: 1, records };
|
|
124
|
+
await writeFile(abs, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
125
|
+
return { path: ledgerRelPath(slug) };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Initialise a ledger for a freshly-applied blueprint. One record
|
|
130
|
+
* per AC contributed by the blueprint. Existing ledger (re-apply)
|
|
131
|
+
* is left byte-identical so operator disposition edits are never
|
|
132
|
+
* clobbered; the return value carries `alreadyExisted: true`.
|
|
133
|
+
*
|
|
134
|
+
* A `fixed` AC (per rcf-schemas 0.6.2 `$defs.acDisposition`) lands as
|
|
135
|
+
* `accepted` with the sentinel reason `fixed-mechanism-inherited`,
|
|
136
|
+
* per spec section 3.6.
|
|
137
|
+
*
|
|
138
|
+
* @param {object} args
|
|
139
|
+
* @param {string} args.projectRoot
|
|
140
|
+
* @param {string} args.slug
|
|
141
|
+
* @param {Array<{ id: string, storyId: string, sourceDisposition?: 'fixed'|'template' }>} args.acDescriptors
|
|
142
|
+
* @param {Date} [args.now]
|
|
143
|
+
* @returns {Promise<{ path: string, alreadyExisted: boolean, recordCount: number }>}
|
|
144
|
+
*/
|
|
145
|
+
export async function initialiseLedger({ projectRoot, slug, acDescriptors, now = new Date() }) {
|
|
146
|
+
if (await ledgerExists(projectRoot, slug)) {
|
|
147
|
+
const existing = await readLedger(projectRoot, slug);
|
|
148
|
+
return { path: ledgerRelPath(slug), alreadyExisted: true, recordCount: existing?.records?.length ?? 0 };
|
|
149
|
+
}
|
|
150
|
+
const iso = now.toISOString().slice(0, 10);
|
|
151
|
+
const records = acDescriptors.map((d) => {
|
|
152
|
+
const base = { acId: d.id, storyId: d.storyId };
|
|
153
|
+
if (d.sourceDisposition === 'fixed') {
|
|
154
|
+
return { ...base, sourceDisposition: 'fixed', action: 'accepted', reason: 'fixed-mechanism-inherited', resolvedAt: iso, resolvedBy: 'blueprint-author' };
|
|
155
|
+
}
|
|
156
|
+
if (d.sourceDisposition === 'template') {
|
|
157
|
+
return { ...base, sourceDisposition: 'template', action: 'pending-disposition' };
|
|
158
|
+
}
|
|
159
|
+
return { ...base, action: 'pending-disposition' };
|
|
160
|
+
});
|
|
161
|
+
const res = await writeLedger({ projectRoot, slug, records });
|
|
162
|
+
if ('error' in res) throw new Error(`ledger init failed: ${res.error}`);
|
|
163
|
+
return { path: res.path, alreadyExisted: false, recordCount: records.length };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Update or append a single AC record. If the acId is present, its
|
|
168
|
+
* fields are merged (later fields win); otherwise the record is
|
|
169
|
+
* appended. Returns { updated: boolean, path } on success.
|
|
170
|
+
*/
|
|
171
|
+
export async function upsertLedgerRecord({ projectRoot, slug, record }) {
|
|
172
|
+
const err = validateRecord(record);
|
|
173
|
+
if (err) return { error: err };
|
|
174
|
+
const doc = (await readLedger(projectRoot, slug)) ?? { slug, schemaVersion: 1, records: [] };
|
|
175
|
+
let updated = false;
|
|
176
|
+
const next = doc.records.map((r) => {
|
|
177
|
+
if (r.acId === record.acId) { updated = true; return { ...r, ...record }; }
|
|
178
|
+
return r;
|
|
179
|
+
});
|
|
180
|
+
if (!updated) next.push(record);
|
|
181
|
+
const res = await writeLedger({ projectRoot, slug, records: next });
|
|
182
|
+
if ('error' in res) return res;
|
|
183
|
+
return { updated, path: res.path };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// The prompt the operator sees at apply time. Kept a canonical string
|
|
187
|
+
// so tests can assert against it and the dispatch spec can echo it
|
|
188
|
+
// verbatim. Contains no em-dashes and no internal identifiers so it
|
|
189
|
+
// reads cleanly in third-party output.
|
|
190
|
+
export const APPLY_DISPOSITION_PROMPT = (slug, version, count) => (
|
|
191
|
+
`Blueprint '${slug}' v${version} contributed ${count} acceptance criteria. `
|
|
192
|
+
+ `For each, you or the applying agent must record a disposition (accept as authored, adjust with a stated reason, `
|
|
193
|
+
+ `drop with a stated reason, or escalate to the operator). `
|
|
194
|
+
+ `Run 'rcf define blueprint dispositions ${slug}' at any time to see the current state; `
|
|
195
|
+
+ `the applying agent will drive the walk unless you want to do it yourself.`
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Ledger schema documentation the follow-up rcf-schemas PR will land.
|
|
200
|
+
* Kept close to the writer so the local guard and the eventual schema
|
|
201
|
+
* definition stay in step.
|
|
202
|
+
*/
|
|
203
|
+
export const LEDGER_SCHEMA_NOTE = `
|
|
204
|
+
The per-slug disposition ledger is a JSON object with:
|
|
205
|
+
{ slug: string,
|
|
206
|
+
schemaVersion: 1,
|
|
207
|
+
records: [
|
|
208
|
+
{ acId: string,
|
|
209
|
+
storyId: string,
|
|
210
|
+
sourceDisposition?: 'fixed' | 'template',
|
|
211
|
+
action: 'pending-disposition' | 'pending-operator' | 'accepted' | 'adjusted' | 'dropped' | 'escalated-as-defect',
|
|
212
|
+
reason?: string,
|
|
213
|
+
resolvedAt?: string (ISO date),
|
|
214
|
+
resolvedBy?: string,
|
|
215
|
+
escalatedAt?: string (ISO date),
|
|
216
|
+
escalatedToOperator?: boolean,
|
|
217
|
+
defectUrl?: string
|
|
218
|
+
}, ...
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
Follow-up: a $defs/blueprintDispositionLedger schema on @stravica-ai/rcf-schemas
|
|
222
|
+
will make the shape validator-checkable; today the ledger writer guards it
|
|
223
|
+
inline (see validateRecord).
|
|
224
|
+
`.trim();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// `rcf define blueprint dispositions <slug> [--json]` sub-verb.
|
|
2
|
+
// Read-only view over the per-slug disposition ledger written by
|
|
3
|
+
// `disposition-ledger.js`. Emits either a human-readable table (one
|
|
4
|
+
// AC per line) or a JSON dump.
|
|
5
|
+
|
|
6
|
+
import { readLedger, ledgerRelPath } from './disposition-ledger.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {object} args
|
|
10
|
+
* @param {string} args.projectRoot
|
|
11
|
+
* @param {string} args.slug
|
|
12
|
+
* @param {boolean} [args.asJson]
|
|
13
|
+
* @returns {Promise<{ output: string, exitCode: number }>}
|
|
14
|
+
*/
|
|
15
|
+
export async function renderDispositions({ projectRoot, slug, asJson = false }) {
|
|
16
|
+
const doc = await readLedger(projectRoot, slug);
|
|
17
|
+
if (!doc) {
|
|
18
|
+
if (asJson) {
|
|
19
|
+
return {
|
|
20
|
+
output: `${JSON.stringify({ slug, present: false, ledgerPath: ledgerRelPath(slug), records: [] }, null, 2)}\n`,
|
|
21
|
+
exitCode: 0,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
output: `[blueprint] no disposition ledger found for '${slug}' at ${ledgerRelPath(slug)}\n`,
|
|
26
|
+
exitCode: 0,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (asJson) {
|
|
30
|
+
return {
|
|
31
|
+
output: `${JSON.stringify({ slug, present: true, ledgerPath: ledgerRelPath(slug), records: doc.records }, null, 2)}\n`,
|
|
32
|
+
exitCode: 0,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const lines = [`[blueprint] '${slug}' disposition ledger (${doc.records.length} record(s)) at ${ledgerRelPath(slug)}:`];
|
|
36
|
+
for (const rec of doc.records) {
|
|
37
|
+
const parts = [rec.acId, rec.storyId, rec.action];
|
|
38
|
+
if (rec.reason) parts.push(`reason: ${rec.reason}`);
|
|
39
|
+
if (rec.resolvedAt) parts.push(`resolvedAt: ${rec.resolvedAt}`);
|
|
40
|
+
if (rec.escalatedAt) parts.push(`escalatedAt: ${rec.escalatedAt}`);
|
|
41
|
+
lines.push(` ${parts.join(' ')}`);
|
|
42
|
+
}
|
|
43
|
+
return { output: `${lines.join('\n')}\n`, exitCode: 0 };
|
|
44
|
+
}
|
package/src/blueprint/index.js
CHANGED
|
@@ -62,3 +62,22 @@ export {
|
|
|
62
62
|
parseUstar,
|
|
63
63
|
sha256Hex,
|
|
64
64
|
} from './library-fetcher-tarball.js';
|
|
65
|
+
export {
|
|
66
|
+
loadForLint,
|
|
67
|
+
loadSuppressions,
|
|
68
|
+
runLint,
|
|
69
|
+
runPass1,
|
|
70
|
+
runPass2,
|
|
71
|
+
} from './consistency-lint.js';
|
|
72
|
+
export {
|
|
73
|
+
APPLY_DISPOSITION_PROMPT,
|
|
74
|
+
LEDGER_SCHEMA_NOTE,
|
|
75
|
+
initialiseLedger,
|
|
76
|
+
ledgerAbsPath,
|
|
77
|
+
ledgerExists,
|
|
78
|
+
ledgerRelPath,
|
|
79
|
+
readLedger,
|
|
80
|
+
upsertLedgerRecord,
|
|
81
|
+
writeLedger,
|
|
82
|
+
} from './disposition-ledger.js';
|
|
83
|
+
export { renderDispositions } from './dispositions.js';
|
|
@@ -94,7 +94,10 @@ export function composeBrowserVerificationRecord({
|
|
|
94
94
|
invariantChecks, authSmokeChecks = [], probePacks = [], notes, now = new Date(),
|
|
95
95
|
}) {
|
|
96
96
|
const id = nextBrowserVerificationId(manifest, fbsId);
|
|
97
|
-
const
|
|
97
|
+
const composedProbePacks = Array.isArray(probePacks)
|
|
98
|
+
? probePacks.map(remapPackForPositiveEvidence)
|
|
99
|
+
: probePacks;
|
|
100
|
+
const verdict = aggregateVerdict(invariantChecks, authSmokeChecks, composedProbePacks);
|
|
98
101
|
const record = {
|
|
99
102
|
id,
|
|
100
103
|
fbsId,
|
|
@@ -107,11 +110,59 @@ export function composeBrowserVerificationRecord({
|
|
|
107
110
|
verdict,
|
|
108
111
|
};
|
|
109
112
|
if (authSmokeChecks && authSmokeChecks.length > 0) record.authSmokeChecks = authSmokeChecks;
|
|
110
|
-
if (Array.isArray(
|
|
113
|
+
if (Array.isArray(composedProbePacks) && composedProbePacks.length > 0) record.probePacks = composedProbePacks;
|
|
111
114
|
if (typeof notes === 'string' && notes.length > 0) record.notes = notes;
|
|
112
115
|
return record;
|
|
113
116
|
}
|
|
114
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Positive-evidence remap (authoring standard section 7d / section 8c).
|
|
120
|
+
*
|
|
121
|
+
* A `checks[]` or `preChecks[]` record with `verdict: 'pass'` is legal
|
|
122
|
+
* only when it carries either an `evidence` field of one of the four
|
|
123
|
+
* shapes named in section 7d (a request id, a response body excerpt, a
|
|
124
|
+
* created-then-deleted resource id in an inventory diff, or a
|
|
125
|
+
* rendered-bytes hash), or an `accountBoundSkipped: true` field with a
|
|
126
|
+
* non-empty `reason`. A pass record with neither is remapped to
|
|
127
|
+
* `verdict: 'fail'` with `detail: 'positive-evidence-missing'` at
|
|
128
|
+
* record-composition time; the composed record is what the manifest
|
|
129
|
+
* carries so the fail surfaces at the ship gate.
|
|
130
|
+
*
|
|
131
|
+
* @param {object} pack
|
|
132
|
+
* @returns {object}
|
|
133
|
+
*/
|
|
134
|
+
function remapPackForPositiveEvidence(pack) {
|
|
135
|
+
if (!pack || typeof pack !== 'object') return pack;
|
|
136
|
+
const nextPack = { ...pack };
|
|
137
|
+
if (Array.isArray(pack.checks)) {
|
|
138
|
+
nextPack.checks = pack.checks.map(remapCheckForPositiveEvidence);
|
|
139
|
+
}
|
|
140
|
+
if (Array.isArray(pack.preChecks)) {
|
|
141
|
+
nextPack.preChecks = pack.preChecks.map(remapCheckForPositiveEvidence);
|
|
142
|
+
}
|
|
143
|
+
return nextPack;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function remapCheckForPositiveEvidence(check) {
|
|
147
|
+
if (!check || typeof check !== 'object') return check;
|
|
148
|
+
if (check.verdict !== 'pass') return check;
|
|
149
|
+
if (checkCarriesPositiveEvidence(check)) return check;
|
|
150
|
+
return { ...check, verdict: 'fail', detail: 'positive-evidence-missing' };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function checkCarriesPositiveEvidence(check) {
|
|
154
|
+
if (check.accountBoundSkipped === true) {
|
|
155
|
+
const reason = check.reason;
|
|
156
|
+
if (typeof reason === 'string' && reason.length > 0) return true;
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
if (check.evidence != null) {
|
|
160
|
+
if (typeof check.evidence === 'string') return check.evidence.length > 0;
|
|
161
|
+
if (typeof check.evidence === 'object') return true;
|
|
162
|
+
}
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
115
166
|
function stripSeverity(check) {
|
|
116
167
|
const out = { invariant: check.invariant, verdict: check.verdict };
|
|
117
168
|
if (typeof check.detail === 'string' && check.detail.length > 0) out.detail = check.detail;
|
|
@@ -81,10 +81,16 @@ export async function runProbePacksForFbs({
|
|
|
81
81
|
for (const pre of pack.preChecks) {
|
|
82
82
|
let verdict = 'pass';
|
|
83
83
|
let detail;
|
|
84
|
+
let preEvidence;
|
|
85
|
+
let preAccountBoundSkipped;
|
|
86
|
+
let preReason;
|
|
84
87
|
try {
|
|
85
88
|
const outcome = await pre.run({ fbs, uiBaseline, manifest, runtimeUrl, fetch, browser });
|
|
86
89
|
verdict = outcome?.verdict ?? 'pass';
|
|
87
90
|
detail = outcome?.detail;
|
|
91
|
+
preEvidence = outcome?.evidence;
|
|
92
|
+
preAccountBoundSkipped = outcome?.accountBoundSkipped;
|
|
93
|
+
preReason = outcome?.reason;
|
|
88
94
|
} catch (err) {
|
|
89
95
|
verdict = 'fail';
|
|
90
96
|
detail = `pre-check threw: ${err.message}`;
|
|
@@ -94,6 +100,9 @@ export async function runProbePacksForFbs({
|
|
|
94
100
|
verdict,
|
|
95
101
|
severity: pre.severity,
|
|
96
102
|
...(typeof detail === 'string' && detail.length > 0 ? { detail } : {}),
|
|
103
|
+
...(preEvidence != null ? { evidence: preEvidence } : {}),
|
|
104
|
+
...(preAccountBoundSkipped === true ? { accountBoundSkipped: true } : {}),
|
|
105
|
+
...(typeof preReason === 'string' && preReason.length > 0 ? { reason: preReason } : {}),
|
|
97
106
|
});
|
|
98
107
|
if (verdict === 'fail') failedPreCheckIds.add(pre.id);
|
|
99
108
|
if (verdict === 'fail' && pre.severity === 'block') highest = 'block';
|
|
@@ -154,10 +163,16 @@ export async function runProbePacksForFbs({
|
|
|
154
163
|
: { browser, fetch, runtimeUrl, route: primaryRoute, theme: primaryTheme, projectRoot };
|
|
155
164
|
let verdict = 'pass';
|
|
156
165
|
let detail;
|
|
166
|
+
let checkEvidence;
|
|
167
|
+
let checkAccountBoundSkipped;
|
|
168
|
+
let checkReason;
|
|
157
169
|
try {
|
|
158
170
|
const outcome = await check.run(context);
|
|
159
171
|
verdict = outcome?.verdict ?? 'pass';
|
|
160
172
|
detail = outcome?.detail;
|
|
173
|
+
checkEvidence = outcome?.evidence;
|
|
174
|
+
checkAccountBoundSkipped = outcome?.accountBoundSkipped;
|
|
175
|
+
checkReason = outcome?.reason;
|
|
161
176
|
} catch (err) {
|
|
162
177
|
verdict = 'fail';
|
|
163
178
|
detail = `check threw: ${err.message}`;
|
|
@@ -167,6 +182,9 @@ export async function runProbePacksForFbs({
|
|
|
167
182
|
verdict,
|
|
168
183
|
severity: check.severity,
|
|
169
184
|
...(typeof detail === 'string' && detail.length > 0 ? { detail } : {}),
|
|
185
|
+
...(checkEvidence != null ? { evidence: checkEvidence } : {}),
|
|
186
|
+
...(checkAccountBoundSkipped === true ? { accountBoundSkipped: true } : {}),
|
|
187
|
+
...(typeof checkReason === 'string' && checkReason.length > 0 ? { reason: checkReason } : {}),
|
|
170
188
|
});
|
|
171
189
|
if (verdict === 'fail' && check.severity === 'block') highest = 'block';
|
|
172
190
|
else if (verdict === 'fail' && check.severity === 'warn' && highest !== 'block') highest = 'warn';
|
package/src/cli/blueprint.js
CHANGED
|
@@ -36,6 +36,12 @@ import {
|
|
|
36
36
|
} from '../blueprint/index.js';
|
|
37
37
|
import { conflictReportJson, renderConflictReport } from '../blueprint/conflicts.js';
|
|
38
38
|
import { handleLibraryVerb, LIBRARY_HELP } from './blueprint-library.js';
|
|
39
|
+
import {
|
|
40
|
+
loadForLint,
|
|
41
|
+
loadSuppressions,
|
|
42
|
+
runLint,
|
|
43
|
+
renderDispositions,
|
|
44
|
+
} from '../blueprint/index.js';
|
|
39
45
|
|
|
40
46
|
export const HELP = `Usage: rcf define blueprint <verb> [options]
|
|
41
47
|
|
|
@@ -115,6 +121,23 @@ Verbs:
|
|
|
115
121
|
'rcf define blueprint library --help' for the
|
|
116
122
|
full surface. Phase 2b covers local sources;
|
|
117
123
|
network fetchers land in Phase 2c.
|
|
124
|
+
lint-consistency <source>
|
|
125
|
+
Run the single-definition ownership and REQ
|
|
126
|
+
delivery lint over one blueprint source (pass 1
|
|
127
|
+
catches literals restated outside their owning
|
|
128
|
+
TAC; pass 2 catches REQs promising properties
|
|
129
|
+
with no delivering TAC or ADR). Exit 0 on
|
|
130
|
+
clean, exit 3 on unsuppressed findings, exit 2
|
|
131
|
+
on usage error. --json emits the machine
|
|
132
|
+
envelope; pass-1 findings are suppressible via
|
|
133
|
+
a "Known chain-consistency-lint suppressions"
|
|
134
|
+
section in the blueprint README (see spec
|
|
135
|
+
section 5.8).
|
|
136
|
+
dispositions <slug> Read the per-slug disposition ledger at
|
|
137
|
+
rcf/blueprints/<slug>.disposition.json. Prints
|
|
138
|
+
one line per contributed AC with its recorded
|
|
139
|
+
disposition; --json emits the raw ledger.
|
|
140
|
+
Read-only.
|
|
118
141
|
|
|
119
142
|
Options:
|
|
120
143
|
--namespace <slug> Override the blueprint's default namespace
|
|
@@ -204,6 +227,11 @@ const OPTION_SPEC = {
|
|
|
204
227
|
'allow-no-queue-yet': { type: 'boolean' },
|
|
205
228
|
answer: { type: 'string', multiple: true },
|
|
206
229
|
answers: { type: 'string' },
|
|
230
|
+
// Integration and contradiction protocol (spec 2026-09-09 section
|
|
231
|
+
// 3.6 and 7.1): the operator (or the wrapping harness) can suppress
|
|
232
|
+
// the disposition prompt printed after a fresh apply. The ledger is
|
|
233
|
+
// still written; only the human-facing prompt line is skipped.
|
|
234
|
+
'no-disposition-prompt': { type: 'boolean' },
|
|
207
235
|
};
|
|
208
236
|
|
|
209
237
|
/**
|
|
@@ -244,6 +272,14 @@ export async function main(argv, deps = {}) {
|
|
|
244
272
|
return handleLibraryVerb(parsed, rest, { stdout, stderr, cwd, now, stdin: deps.stdin });
|
|
245
273
|
}
|
|
246
274
|
|
|
275
|
+
// `lint-consistency <source>` runs over a blueprint source directory
|
|
276
|
+
// and does not require an rcf/ tree; wire it before the projectRoot
|
|
277
|
+
// walk so it stays usable from a fresh clone (and from the shipping
|
|
278
|
+
// CI gate).
|
|
279
|
+
if (verb === 'lint-consistency') {
|
|
280
|
+
return handleLintConsistencyVerb({ rest, parsed, cwd, stdout, stderr });
|
|
281
|
+
}
|
|
282
|
+
|
|
247
283
|
const projectRoot = await findProjectRoot(cwd);
|
|
248
284
|
if (!projectRoot) {
|
|
249
285
|
stderr.write('[error] no rcf/ tree found in this directory or any ancestor.\n');
|
|
@@ -433,6 +469,13 @@ export async function main(argv, deps = {}) {
|
|
|
433
469
|
}
|
|
434
470
|
if (!parsed.values.quiet) {
|
|
435
471
|
stdout.write(`[blueprint] applied '${result.slug}' at ${result.version} (${result.contributions.length} contribution(s)).\n`);
|
|
472
|
+
// Disposition prompt (spec section 3.6 + 3.7). Printed once per
|
|
473
|
+
// fresh apply; a re-apply on the same slug omits it because the
|
|
474
|
+
// ledger is left untouched. Suppressible with the CLI flag
|
|
475
|
+
// --no-disposition-prompt.
|
|
476
|
+
if (result.dispositionPrompt && parsed.values['no-disposition-prompt'] !== true) {
|
|
477
|
+
stdout.write(`${result.dispositionPrompt}\n`);
|
|
478
|
+
}
|
|
436
479
|
}
|
|
437
480
|
// Companion-suggestion mechanism (spec 2.6). Runs AFTER the apply
|
|
438
481
|
// writes so a failed apply never produces a suggestion block.
|
|
@@ -614,6 +657,17 @@ export async function main(argv, deps = {}) {
|
|
|
614
657
|
});
|
|
615
658
|
}
|
|
616
659
|
|
|
660
|
+
if (verb === 'dispositions') {
|
|
661
|
+
if (rest.length === 0) {
|
|
662
|
+
stderr.write('[error] blueprint dispositions: missing <slug>\n');
|
|
663
|
+
return 2;
|
|
664
|
+
}
|
|
665
|
+
const slug = rest[0];
|
|
666
|
+
const rendered = await renderDispositions({ projectRoot, slug, asJson: parsed.values.json === true });
|
|
667
|
+
stdout.write(rendered.output);
|
|
668
|
+
return rendered.exitCode;
|
|
669
|
+
}
|
|
670
|
+
|
|
617
671
|
stderr.write(`[error] blueprint: unknown verb '${verb}'\n`);
|
|
618
672
|
stderr.write(HELP);
|
|
619
673
|
return 2;
|
|
@@ -850,6 +904,55 @@ function parseResolveOptions(rawList, reason) {
|
|
|
850
904
|
* @param {{ stdin: NodeJS.ReadableStream, stdout: NodeJS.WritableStream }} deps
|
|
851
905
|
* @returns {Promise<(prompt: string) => Promise<string>>}
|
|
852
906
|
*/
|
|
907
|
+
/**
|
|
908
|
+
* `rcf define blueprint lint-consistency <source>` handler. Resolves
|
|
909
|
+
* <source> (path or shelf slug), loads the blueprint via loadForLint,
|
|
910
|
+
* reads any suppressions in the blueprint's README, runs both passes,
|
|
911
|
+
* and prints either a human summary or a JSON envelope.
|
|
912
|
+
*
|
|
913
|
+
* Exit codes:
|
|
914
|
+
* 0 clean (or every finding suppressed)
|
|
915
|
+
* 2 usage error / cannot resolve source
|
|
916
|
+
* 3 unsuppressed findings
|
|
917
|
+
*/
|
|
918
|
+
async function handleLintConsistencyVerb({ rest, parsed, cwd, stdout, stderr }) {
|
|
919
|
+
if (rest.length === 0) {
|
|
920
|
+
stderr.write('[error] blueprint lint-consistency: missing <source>\n');
|
|
921
|
+
return 2;
|
|
922
|
+
}
|
|
923
|
+
const rawSource = rest[0];
|
|
924
|
+
// Reuse the shipping shelf resolver so a bare slug (edge-cloudflare-turnstile)
|
|
925
|
+
// or a @stock/<slug> reference resolves to the packaged shelf, and a
|
|
926
|
+
// path resolves relative to cwd.
|
|
927
|
+
const resolved = await resolveBlueprintSource(rawSource, { projectRoot: cwd });
|
|
928
|
+
if (isRcfError(resolved)) {
|
|
929
|
+
stderr.write(`[error] blueprint lint-consistency: ${resolved.message}\n`);
|
|
930
|
+
return 2;
|
|
931
|
+
}
|
|
932
|
+
const sourcePath = resolved.resolved;
|
|
933
|
+
const loaded = await loadForLint(sourcePath);
|
|
934
|
+
if ('error' in loaded) {
|
|
935
|
+
stderr.write(`[error] blueprint lint-consistency: ${loaded.error}\n`);
|
|
936
|
+
return 2;
|
|
937
|
+
}
|
|
938
|
+
const suppressions = await loadSuppressions(sourcePath);
|
|
939
|
+
const result = runLint(loaded, suppressions);
|
|
940
|
+
const asJson = parsed.values.json === true;
|
|
941
|
+
if (asJson) {
|
|
942
|
+
stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
943
|
+
} else {
|
|
944
|
+
stdout.write(`[blueprint] lint-consistency '${result.blueprint}': ${result.verdict} `
|
|
945
|
+
+ `(pass1=${result.passCounts.pass1}, pass2=${result.passCounts.pass2}, `
|
|
946
|
+
+ `suppressed=${result.passCounts.suppressed})\n`);
|
|
947
|
+
for (const f of result.findings) {
|
|
948
|
+
const tag = f.suppressed ? '[suppressed]' : '[finding]';
|
|
949
|
+
stdout.write(` ${tag} ${f.id} (${f.pass}, ${f.kind}): ${f.message}\n`);
|
|
950
|
+
if (f.suppressed && f.suppressionReason) stdout.write(` reason: ${f.suppressionReason}\n`);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return result.verdict === 'pass' ? 0 : 3;
|
|
954
|
+
}
|
|
955
|
+
|
|
853
956
|
async function createCustomAuthReadLine({ stdin, stdout }) {
|
|
854
957
|
const { createInterface } = await import('node:readline');
|
|
855
958
|
const rl = createInterface({ input: stdin, output: stdout, terminal: true });
|
|
@@ -57,6 +57,24 @@ _(how you like to work: casual or formal register, verbose or terse
|
|
|
57
57
|
responses, willingness to be pushed back on, anything the agent should
|
|
58
58
|
know before choosing its default posture)_
|
|
59
59
|
|
|
60
|
+
## Register
|
|
61
|
+
|
|
62
|
+
_(how the agent should frame decisions when it needs your ruling. One
|
|
63
|
+
of the three values below on a single line; a line that reads only
|
|
64
|
+
\`unstated\` is fine and reads as productOwner on the first
|
|
65
|
+
escalation.)_
|
|
66
|
+
|
|
67
|
+
- \`productOwner\`: escalations arrive as one plain-English sentence
|
|
68
|
+
naming the decision and its consequences, then two or three named
|
|
69
|
+
choices with what each costs. No method vocabulary, no document ids.
|
|
70
|
+
- \`engineer\`: escalations name the AC id, quote the seed's assertion,
|
|
71
|
+
quote the project convention that conflicts, and ask a direct
|
|
72
|
+
question. Fluent in the method vocabulary.
|
|
73
|
+
- \`unstated\`: the agent asks you which register on the first
|
|
74
|
+
exchange after init and holds \`productOwner\` in the meantime.
|
|
75
|
+
|
|
76
|
+
unstated
|
|
77
|
+
|
|
60
78
|
## Project-scoped notes
|
|
61
79
|
|
|
62
80
|
_(anything specific to this project that would be useful for the agent
|