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.
Files changed (137) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/blueprints/application-account-settings/docs/topics.md +1 -1
  3. package/blueprints/application-admin-console/docs/topics.md +1 -1
  4. package/blueprints/application-api-rest/docs/topics.md +1 -1
  5. package/blueprints/application-charts/docs/topics.md +1 -1
  6. package/blueprints/application-dashboard/docs/topics.md +1 -1
  7. package/blueprints/application-datatable/docs/topics.md +1 -1
  8. package/blueprints/application-empty-error-states/docs/topics.md +1 -1
  9. package/blueprints/application-error-handling/docs/topics.md +1 -1
  10. package/blueprints/application-file-upload/docs/topics.md +1 -1
  11. package/blueprints/application-forms-wizard/docs/topics.md +1 -1
  12. package/blueprints/application-notifications-in-app/docs/topics.md +1 -1
  13. package/blueprints/application-onboarding-tour/docs/topics.md +1 -1
  14. package/blueprints/application-spa/docs/topics.md +1 -1
  15. package/blueprints/delivery-ci-workflows/docs/topics.md +1 -1
  16. package/blueprints/deploy-cloudflare-workers/docs/topics.md +1 -1
  17. package/blueprints/deploy-hetzner-server/CHANGELOG.md +21 -0
  18. package/blueprints/deploy-hetzner-server/blueprint.json +1 -1
  19. package/blueprints/deploy-hetzner-server/contributions/probes/cloud-init-render-lint.mjs +12 -5
  20. package/blueprints/deploy-hetzner-server/contributions/probes/hcloud-dry-run-mock.mjs +68 -20
  21. package/blueprints/deploy-hetzner-server/contributions/probes/manifest-schema-validate.mjs +8 -6
  22. package/blueprints/deploy-hetzner-server/contributions/probes/probe-utils.mjs +7 -1
  23. package/blueprints/deploy-hetzner-server/contributions/probes/real-account-cloud-init-hardened.mjs +2 -1
  24. package/blueprints/deploy-hetzner-server/contributions/templates/cloud-init.yaml.tmpl +26 -6
  25. package/blueprints/deploy-hetzner-server/docs/topics.md +1 -1
  26. package/blueprints/edge-cloudflare-access/docs/topics.md +1 -1
  27. package/blueprints/edge-cloudflare-rate-limiting/docs/topics.md +1 -1
  28. package/blueprints/edge-cloudflare-tunnel/CHANGELOG.md +10 -0
  29. package/blueprints/edge-cloudflare-tunnel/blueprint.json +1 -1
  30. package/blueprints/edge-cloudflare-tunnel/contributions/probes/manifest-schema-validate.mjs +4 -1
  31. package/blueprints/edge-cloudflare-tunnel/contributions/probes/real-account-connector-healthy.mjs +110 -21
  32. package/blueprints/edge-cloudflare-tunnel/contributions/probes/real-account-tunnel-hostname-routes.mjs +123 -30
  33. package/blueprints/edge-cloudflare-tunnel/docs/topics.md +1 -1
  34. package/blueprints/edge-cloudflare-turnstile/docs/topics.md +1 -1
  35. package/blueprints/email-smtp-resend/docs/topics.md +1 -1
  36. package/blueprints/jobs-background/docs/topics.md +1 -1
  37. package/blueprints/messaging-queue-cloudflare/CHANGELOG.md +19 -0
  38. package/blueprints/messaging-queue-cloudflare/README.md +2 -2
  39. package/blueprints/messaging-queue-cloudflare/blueprint.json +1 -1
  40. package/blueprints/messaging-queue-cloudflare/contributions/probes/real-account-concurrency-smoke.mjs +192 -37
  41. package/blueprints/messaging-queue-cloudflare/docs/topics.md +1 -1
  42. package/blueprints/object-storage-s3/CHANGELOG.md +11 -0
  43. package/blueprints/object-storage-s3/README.md +14 -1
  44. package/blueprints/object-storage-s3/blueprint.json +12 -1
  45. package/blueprints/object-storage-s3/contributions/adrs/adr-2905-object-storage-s3-hetzner-object-storage-provider.json +30 -0
  46. package/blueprints/object-storage-s3/contributions/probes/hetzner-object-storage-round-trip.mjs +136 -0
  47. package/blueprints/object-storage-s3/contributions/probes/run-hetzner-object-storage-round-trip.mjs +11 -0
  48. package/blueprints/object-storage-s3/contributions/requirements/object-storage-s3-req-101.json +18 -0
  49. package/blueprints/object-storage-s3/contributions/tacs/tac-2904-object-storage-s3-hetzner-endpoint-helper.json +34 -0
  50. package/blueprints/object-storage-s3/contributions/user-stories/object-storage-s3-us-28110.json +25 -0
  51. package/blueprints/object-storage-s3/docs/topics.md +1 -1
  52. package/blueprints/object-storage-s3/guide/object-storage-s3.md +36 -0
  53. package/blueprints/observability-essentials/docs/topics.md +1 -1
  54. package/blueprints/observability-logging/docs/topics.md +1 -1
  55. package/blueprints/observability-probe-endpoints/docs/topics.md +1 -1
  56. package/blueprints/persistence-data-d1/docs/topics.md +1 -1
  57. package/blueprints/persistence-data-postgres/docs/topics.md +1 -1
  58. package/blueprints/persistence-data-sqlite/docs/topics.md +1 -1
  59. package/blueprints/platform-cloudflare-cron-triggers/docs/topics.md +1 -1
  60. package/blueprints/platform-cloudflare-durable-objects/CHANGELOG.md +15 -0
  61. package/blueprints/platform-cloudflare-durable-objects/blueprint.json +1 -1
  62. package/blueprints/platform-cloudflare-durable-objects/contributions/probes/namespace-facade-ready.mjs +3 -3
  63. package/blueprints/platform-cloudflare-durable-objects/contributions/probes/probe-utils.mjs +28 -14
  64. package/blueprints/platform-cloudflare-durable-objects/contributions/probes/real-account-storage-smoke.mjs +132 -26
  65. package/blueprints/platform-cloudflare-durable-objects/contributions/probes/sole-reader-scan.mjs +40 -39
  66. package/blueprints/platform-cloudflare-durable-objects/contributions/probes/storage-round-trip.mjs +26 -16
  67. package/blueprints/platform-cloudflare-durable-objects/contributions/probes/websocket-hub-broadcast.mjs +31 -34
  68. package/blueprints/platform-cloudflare-durable-objects/docs/topics.md +1 -1
  69. package/blueprints/platform-cloudflare-kv/CHANGELOG.md +40 -1
  70. package/blueprints/platform-cloudflare-kv/README.md +41 -13
  71. package/blueprints/platform-cloudflare-kv/blueprint.json +1 -1
  72. package/blueprints/platform-cloudflare-kv/contributions/probes/cache-aside-hit-then-miss.mjs +26 -26
  73. package/blueprints/platform-cloudflare-kv/contributions/probes/event-secrecy.mjs +49 -31
  74. package/blueprints/platform-cloudflare-kv/contributions/probes/facade-round-trip.mjs +58 -16
  75. package/blueprints/platform-cloudflare-kv/contributions/probes/list-with-prefix.mjs +6 -5
  76. package/blueprints/platform-cloudflare-kv/contributions/probes/real-account-eventual-consistency-smoke.mjs +131 -67
  77. package/blueprints/platform-cloudflare-kv/docs/topics.md +1 -1
  78. package/blueprints/platform-docker-compose-host/docs/topics.md +1 -1
  79. package/blueprints/security-auth-clerk/docs/topics.md +1 -1
  80. package/blueprints/security-auth-keycloak/docs/topics.md +1 -1
  81. package/blueprints/security-auth-magic-link/docs/topics.md +1 -1
  82. package/blueprints/security-auth-oauth2/docs/topics.md +1 -1
  83. package/blueprints/security-secrets-management/docs/topics.md +1 -1
  84. package/fixtures/canary-manifest.json +9 -9
  85. package/guidance/harness-template.md +67 -0
  86. package/guidance/managed/agent-instructions-block.hash +1 -1
  87. package/guidance/managed/agent-instructions-block.md +67 -0
  88. package/package.json +2 -2
  89. package/rcf/code-nodes/cn-500.json +12 -0
  90. package/rcf/code-nodes/cn-510.json +12 -0
  91. package/rcf/code-nodes/cn-520.json +14 -0
  92. package/rcf/code-nodes/cn-521.json +12 -0
  93. package/rcf/code-nodes/cn-522.json +13 -0
  94. package/rcf/code-nodes/cn-523.json +12 -0
  95. package/rcf/code-nodes/cn-524.json +14 -0
  96. package/rcf/code-nodes/cn-525.json +12 -0
  97. package/rcf/code-nodes/cn-526.json +14 -0
  98. package/rcf/fbs/fbs-160.json +15 -0
  99. package/rcf/fbs/fbs-165.json +15 -0
  100. package/rcf/fbs/fbs-170.json +16 -0
  101. package/rcf/fbs/fbs-171.json +16 -0
  102. package/rcf/fbs/fbs-172.json +15 -0
  103. package/rcf/fbs/fbs-173.json +17 -0
  104. package/rcf/fbs/fbs-174.json +17 -0
  105. package/rcf/requirements/req-140.json +149 -0
  106. package/rcf/requirements/req-145.json +53 -0
  107. package/rcf/requirements/req-150.json +20 -0
  108. package/rcf/requirements/req-151.json +85 -0
  109. package/rcf/requirements/req-152.json +20 -0
  110. package/rcf/requirements/req-153.json +59 -0
  111. package/rcf/requirements/req-154.json +44 -0
  112. package/rcf/test-suites/ts-071.json +2 -2
  113. package/rcf/test-suites/ts-086.json +2 -2
  114. package/rcf/test-suites/ts-170.json +22 -0
  115. package/rcf/test-suites/ts-175.json +22 -0
  116. package/rcf/test-suites/ts-180.json +30 -0
  117. package/rcf/test-suites/ts-181.json +30 -0
  118. package/rcf/test-suites/ts-182.json +22 -0
  119. package/rcf/test-suites/ts-183.json +38 -0
  120. package/rcf/test-suites/ts-184.json +38 -0
  121. package/rcf/user-stories/us-14001.json +21 -0
  122. package/rcf/user-stories/us-14501.json +24 -0
  123. package/rcf/user-stories/us-15001.json +26 -0
  124. package/rcf/user-stories/us-15101.json +26 -0
  125. package/rcf/user-stories/us-15201.json +21 -0
  126. package/rcf/user-stories/us-15301.json +31 -0
  127. package/rcf/user-stories/us-15401.json +31 -0
  128. package/releases/releases.yaml +11 -1
  129. package/src/blueprint/apply.js +64 -0
  130. package/src/blueprint/consistency-lint.js +432 -0
  131. package/src/blueprint/disposition-ledger.js +224 -0
  132. package/src/blueprint/dispositions.js +44 -0
  133. package/src/blueprint/index.js +19 -0
  134. package/src/browser-verify/manifest-writer.js +53 -2
  135. package/src/browser-verify/pack-runner.js +18 -0
  136. package/src/cli/blueprint.js +103 -0
  137. 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
+ }
@@ -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 verdict = aggregateVerdict(invariantChecks, authSmokeChecks, probePacks);
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(probePacks) && probePacks.length > 0) record.probePacks = probePacks;
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';
@@ -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