rcf-lite 0.30.0 → 0.32.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 (65) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/blueprints/deploy-cloudflare-workers/contributions/user-stories/deploy-cloudflare-workers-us-12113.json +1 -1
  3. package/fixtures/canary-manifest.json +9 -9
  4. package/guidance/define-draft-shapes.md +15 -10
  5. package/package.json +1 -1
  6. package/rcf/adrs/adr-4131-gates-bite-ack-recorded-override.json +3 -3
  7. package/rcf/adrs/adr-4137-standalone-theme-persistence.json +30 -0
  8. package/rcf/adrs/adr-4138-tree-wide-checks-before-scope-shortcircuit.json +35 -0
  9. package/rcf/code-nodes/cn-017.json +4 -2
  10. package/rcf/code-nodes/cn-771.json +15 -0
  11. package/rcf/code-nodes/cn-772.json +14 -0
  12. package/rcf/code-nodes/cn-773.json +16 -0
  13. package/rcf/code-nodes/cn-774.json +20 -0
  14. package/rcf/code-nodes/cn-775.json +15 -0
  15. package/rcf/code-nodes/cn-776.json +17 -0
  16. package/rcf/code-nodes/cn-777.json +21 -0
  17. package/rcf/code-nodes/cn-778.json +14 -0
  18. package/rcf/fbs/fbs-004.json +4 -1
  19. package/rcf/fbs/fbs-201.json +2 -1
  20. package/rcf/fbs/fbs-202.json +18 -2
  21. package/rcf/fbs/fbs-203.json +12 -4
  22. package/rcf/requirements/req-002.json +2 -2
  23. package/rcf/requirements/req-173.json +2 -2
  24. package/rcf/requirements/req-174.json +2 -2
  25. package/rcf/tacs/tac-4122-define-gates.json +7 -2
  26. package/rcf/tacs/tac-4127-readiness-view.json +3 -2
  27. package/rcf/test-suites/ts-004.json +82 -2
  28. package/rcf/test-suites/ts-007.json +1 -1
  29. package/rcf/test-suites/ts-215.json +64 -1
  30. package/rcf/test-suites/ts-216.json +18 -2
  31. package/rcf/test-suites/ts-219.json +10 -2
  32. package/rcf/test-suites/ts-220.json +18 -2
  33. package/rcf/test-suites/ts-223.json +24 -2
  34. package/rcf/test-suites/ts-225.json +10 -2
  35. package/rcf/test-suites/ts-229.json +49 -3
  36. package/rcf/test-suites/ts-230.json +26 -2
  37. package/rcf/test-suites/ts-231.json +16 -1
  38. package/rcf/test-suites/ts-236.json +44 -0
  39. package/rcf/user-stories/us-17302.json +36 -1
  40. package/rcf/user-stories/us-17401.json +11 -1
  41. package/rcf/user-stories/us-17402.json +6 -1
  42. package/rcf/user-stories/us-17403.json +11 -1
  43. package/rcf/user-stories/us-17404.json +16 -1
  44. package/rcf/user-stories/us-17405.json +7 -2
  45. package/rcf/user-stories/us-18001.json +6 -1
  46. package/rcf/user-stories/us-202.json +25 -1
  47. package/rcf/user-stories/us-205.json +6 -1
  48. package/rcf/user-stories/us-206.json +25 -4
  49. package/rcf/user-stories/us-207.json +36 -0
  50. package/releases/releases.yaml +20 -1
  51. package/src/cli/create.js +22 -2
  52. package/src/cli/freeze.js +8 -0
  53. package/src/cli/ledger.js +96 -4
  54. package/src/cli/readiness.js +17 -39
  55. package/src/core/errors/index.js +7 -1
  56. package/src/core/store/writer.js +181 -13
  57. package/src/define/interface-paths.js +47 -0
  58. package/src/define/ledgers.js +86 -7
  59. package/src/mcp/tools.js +1 -1
  60. package/src/query/gates.js +431 -163
  61. package/src/view/html-page.js +43 -13
  62. package/src/view/live-client.js +18 -10
  63. package/src/view/page-init.js +149 -8
  64. package/src/view/style.css +107 -20
  65. package/src/view/view-index.js +22 -0
@@ -102,7 +102,7 @@ export class LedgerError extends Error {
102
102
  * @param {string} message
103
103
  * @param {object} [opts]
104
104
  * @param {string} [opts.filePath]
105
- * @param {'parseFailure' | 'schemaFailure' | 'ioFailure' | 'usage'} [opts.code]
105
+ * @param {'parseFailure' | 'schemaFailure' | 'ioFailure' | 'usage' | 'conflict'} [opts.code]
106
106
  * @param {string} [opts.field]
107
107
  */
108
108
  constructor(message, opts = {}) {
@@ -390,26 +390,105 @@ export function nextIdFor(name, body) {
390
390
  * updated body. Pure over its inputs (does not write to disk); the
391
391
  * CLI calls `saveLedger` with the result.
392
392
  *
393
+ * Ruling R1 2026-10-05 (w-2026-10-05-dave-001): `add` is idempotent by
394
+ * id. When `args.id` is set (the harness carries it on a replayed
395
+ * turn):
396
+ * - no entry with that id exists: honour the override and add the
397
+ * entry at that id (any gaps in the id sequence are legal; the
398
+ * per-ledger validator enforces uniqueness, not contiguity);
399
+ * - entry with that id exists and the content matches: return
400
+ * `{ status: 'unchanged', body, entry }`, no new write;
401
+ * - entry with that id exists and the content differs: throw a
402
+ * `LedgerError` (code 'conflict') naming the id and the differing
403
+ * field keys so the CLI exits 3. Update is a separate verb.
404
+ * When `args.id` is omitted the next id is minted as before (the
405
+ * pre-R1 behaviour; a replayed turn without a carried id still works
406
+ * but duplicates the entry at a new id, which is why the harness is
407
+ * responsible for threading --id back).
408
+ *
393
409
  * @param {object} args
394
410
  * @param {LedgerName} args.name
395
411
  * @param {Record<string, unknown[]>} args.body
396
412
  * @param {Record<string, unknown>} args.entry - fields other than id / addedAt / status
413
+ * @param {number} [args.id] - optional id override (R1)
397
414
  * @param {string} [args.now] - ISO timestamp; defaults to Date.now()
398
- * @returns {{ body: Record<string, unknown[]>, entry: Record<string, unknown> }}
415
+ * @returns {{ body: Record<string, unknown[]>, entry: Record<string, unknown>, status?: 'added' | 'unchanged' }}
399
416
  */
400
- export function addEntry({ name, body, entry, now }) {
417
+ export function addEntry({ name, body, entry, id, now }) {
401
418
  const cfg = LEDGER_CONFIG[name];
402
419
  if (!cfg) throw new LedgerError(`Unknown ledger '${name}'.`, { code: 'usage' });
403
- const id = nextIdFor(name, body);
420
+ const list = /** @type {any[]} */ (body[cfg.arrayKey] ?? []);
421
+ let resolvedId;
422
+ if (id !== undefined) {
423
+ if (!Number.isInteger(id) || id < 1) {
424
+ throw new LedgerError(`${name} add --id must be a positive integer, got ${id}.`, {
425
+ filePath: ledgerRelPath(name), field: 'id', code: 'usage',
426
+ });
427
+ }
428
+ const existing = list.find((e) => Number(e?.id) === Number(id));
429
+ if (existing) {
430
+ // R1 idempotency check: compare caller-supplied fields against the
431
+ // existing entry. `addedAt` and `status` default on first add, so
432
+ // they are compared only when the caller explicitly supplied them.
433
+ const differing = [];
434
+ for (const [key, value] of Object.entries(entry)) {
435
+ if (value === undefined) continue;
436
+ if (key === 'id') continue;
437
+ if (key === 'addedAt' || key === 'status') {
438
+ // Default values auto-populated on first add; skip unless the
439
+ // caller passed them explicitly AND they differ.
440
+ if (existing[key] !== value) differing.push(key);
441
+ continue;
442
+ }
443
+ if (!deepEqual(existing[key], value)) differing.push(key);
444
+ }
445
+ if (differing.length === 0) {
446
+ return { body, entry: existing, status: 'unchanged' };
447
+ }
448
+ throw new LedgerError(
449
+ `${name} ledger entry ${id} already exists with different content; differing fields: ${differing.sort().join(', ')}. Use the update verb to patch.`,
450
+ { filePath: ledgerRelPath(name), field: `id ${id}`, code: 'conflict' },
451
+ );
452
+ }
453
+ resolvedId = id;
454
+ } else {
455
+ resolvedId = nextIdFor(name, body);
456
+ }
404
457
  const full = {
405
- id,
458
+ id: resolvedId,
406
459
  ...entry,
407
460
  addedAt: entry.addedAt ?? now ?? new Date().toISOString(),
408
461
  status: entry.status ?? 'open',
409
462
  };
410
- const nextBody = { ...body, [cfg.arrayKey]: [...(body[cfg.arrayKey] ?? []), full] };
463
+ const nextBody = { ...body, [cfg.arrayKey]: [...list, full] };
411
464
  validateLedger(name, nextBody, ledgerRelPath(name));
412
- return { body: nextBody, entry: full };
465
+ return { body: nextBody, entry: full, status: 'added' };
466
+ }
467
+
468
+ /**
469
+ * Deep-equality over JSON-shaped values. Used by R1's idempotency
470
+ * check to compare a caller-supplied field against the existing entry.
471
+ * Arrays and plain objects are compared structurally; primitives via
472
+ * Object.is. Pure.
473
+ */
474
+ function deepEqual(a, b) {
475
+ if (Object.is(a, b)) return true;
476
+ if (a === null || b === null) return false;
477
+ if (typeof a !== typeof b) return false;
478
+ if (Array.isArray(a)) {
479
+ if (!Array.isArray(b) || a.length !== b.length) return false;
480
+ for (let i = 0; i < a.length; i += 1) if (!deepEqual(a[i], b[i])) return false;
481
+ return true;
482
+ }
483
+ if (typeof a === 'object') {
484
+ if (typeof b !== 'object' || Array.isArray(b)) return false;
485
+ const ak = Object.keys(a);
486
+ const bk = Object.keys(b);
487
+ if (ak.length !== bk.length) return false;
488
+ for (const k of ak) if (!deepEqual(a[k], b[k])) return false;
489
+ return true;
490
+ }
491
+ return false;
413
492
  }
414
493
 
415
494
  /**
package/src/mcp/tools.js CHANGED
@@ -639,7 +639,7 @@ const DEFINITIONS = [
639
639
  properties: {
640
640
  kind: { type: 'string', enum: KIND_ENUM, description: 'Document kind' },
641
641
  parent: { type: 'string', description: 'Parent document id; required for every kind except cn (which has no parent)' },
642
- id: { type: 'string', description: 'Override the auto-assigned id (refuses on collision)' },
642
+ id: { type: 'string', description: 'Override the auto-assigned id. Idempotent by id: an existing id with identical content is a no-op; different content refuses as a conflict naming the differing fields (use rcf_update to patch)' },
643
643
  title: { type: 'string', description: 'Required for req / us / tac / adr / fbs / ts' },
644
644
  description: { type: 'string', description: 'Required for ac / tc' },
645
645
  purpose: { type: 'string', description: 'Required for ts' },