rcf-lite 0.16.0 → 0.18.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 +36 -0
- package/blueprints/application-api-rest/README.md +5 -1
- package/blueprints/application-api-rest/blueprint.json +1 -1
- package/blueprints/application-api-rest/contributions/requirements/application-api-rest-req-006.json +4 -4
- package/blueprints/application-api-rest/contributions/tacs/tac-306-application-api-rest-operability.json +9 -8
- package/blueprints/application-api-rest/contributions/user-stories/application-api-rest-us-2103.json +3 -3
- package/blueprints/application-api-rest/contributions/user-stories/application-api-rest-us-2108.json +27 -27
- package/blueprints/application-api-rest/docs/topics.md +2 -2
- package/blueprints/application-api-rest/guide/application-api-rest.md +5 -1
- package/blueprints/application-spa/assets/tc-templates/e2e.md +85 -0
- package/blueprints/application-spa/blueprint.json +24 -2
- package/blueprints/application-spa/contributions/user-stories/application-spa-us-1134.json +24 -0
- package/blueprints/application-spa/contributions/user-stories/application-spa-us-1135.json +24 -0
- package/blueprints/application-spa/docs/topics.md +2 -2
- package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/github-actions/pull-request-checks.yml +69 -0
- package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/notes.md +18 -0
- package/blueprints/delivery-ci-workflows/blueprint.json +301 -60
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6124.json +28 -0
- package/blueprints/delivery-ci-workflows/docs/topics.md +2 -2
- package/blueprints/deploy-cloudflare-workers/docs/topics.md +2 -2
- package/blueprints/email-smtp-resend/docs/topics.md +2 -2
- package/blueprints/observability-essentials/README.md +6 -2
- package/blueprints/observability-essentials/blueprint.json +133 -33
- package/blueprints/observability-essentials/contributions/adrs/adr-801-observability-essentials-health-probes.json +4 -4
- package/blueprints/observability-essentials/contributions/adrs/adr-802-observability-essentials-readiness-semantics.json +4 -4
- package/blueprints/observability-essentials/contributions/requirements/observability-essentials-req-001.json +4 -4
- package/blueprints/observability-essentials/contributions/requirements/observability-essentials-req-002.json +4 -4
- package/blueprints/observability-essentials/contributions/tacs/tac-801-observability-essentials-liveness-probe.json +10 -9
- package/blueprints/observability-essentials/contributions/tacs/tac-802-observability-essentials-readiness-probe.json +17 -11
- package/blueprints/observability-essentials/contributions/user-stories/observability-essentials-us-7101.json +12 -3
- package/blueprints/observability-essentials/contributions/user-stories/observability-essentials-us-7102.json +12 -3
- package/blueprints/observability-essentials/docs/topics.md +15 -8
- package/blueprints/observability-essentials/guide/observability-essentials.md +9 -3
- package/blueprints/observability-probe-endpoints/README.md +5 -1
- package/blueprints/observability-probe-endpoints/blueprint.json +116 -24
- package/blueprints/observability-probe-endpoints/contributions/adrs/adr-1503-observability-probe-endpoints-kubernetes-default.json +5 -5
- package/blueprints/observability-probe-endpoints/contributions/user-stories/observability-probe-endpoints-us-14102.json +11 -2
- package/blueprints/observability-probe-endpoints/contributions/user-stories/observability-probe-endpoints-us-14107.json +11 -2
- package/blueprints/observability-probe-endpoints/docs/topics.md +6 -6
- package/blueprints/observability-probe-endpoints/guide/observability-probe-endpoints.md +10 -0
- package/blueprints/persistence-data-d1/docs/topics.md +2 -2
- package/blueprints/persistence-data-sqlite/docs/topics.md +2 -2
- package/blueprints/security-auth-clerk/docs/topics.md +2 -2
- package/blueprints/security-auth-keycloak/docs/topics.md +2 -2
- package/blueprints/security-auth-magic-link/docs/topics.md +2 -2
- package/blueprints/security-auth-oauth2/docs/topics.md +2 -2
- package/blueprints/security-secrets-management/docs/topics.md +2 -2
- package/fixtures/canary-manifest.json +9 -9
- package/package.json +13 -1
- package/rcf/code-nodes/cn-070.json +12 -0
- package/rcf/code-nodes/cn-071.json +12 -0
- package/rcf/code-nodes/cn-072.json +12 -0
- package/rcf/code-nodes/cn-073.json +12 -0
- package/rcf/code-nodes/cn-074.json +19 -0
- package/rcf/code-nodes/cn-075.json +15 -0
- package/rcf/code-nodes/cn-076.json +14 -0
- package/rcf/fbs/fbs-020.json +18 -0
- package/rcf/fbs/fbs-021.json +18 -0
- package/rcf/fbs/fbs-022.json +18 -0
- package/rcf/fbs/fbs-023.json +18 -0
- package/rcf/fbs/fbs-024.json +24 -0
- package/rcf/fbs/fbs-025.json +25 -0
- package/rcf/fbs/fbs-026.json +27 -0
- package/rcf/fbs/fbs-027.json +25 -0
- package/rcf/prd.json +3 -2
- package/rcf/requirements/req-011.json +22 -0
- package/rcf/requirements/req-012.json +22 -0
- package/rcf/test-suites/ts-030.json +66 -0
- package/rcf/test-suites/ts-031.json +59 -0
- package/rcf/test-suites/ts-032.json +50 -0
- package/rcf/test-suites/ts-033.json +83 -0
- package/rcf/test-suites/ts-034.json +32 -0
- package/rcf/test-suites/ts-035.json +23 -0
- package/rcf/test-suites/ts-036.json +65 -0
- package/rcf/test-suites/ts-037.json +55 -0
- package/rcf/user-stories/us-1101.json +51 -0
- package/rcf/user-stories/us-1102.json +60 -0
- package/rcf/user-stories/us-1103.json +51 -0
- package/rcf/user-stories/us-1104.json +60 -0
- package/rcf/user-stories/us-1201.json +34 -0
- package/rcf/user-stories/us-1202.json +25 -0
- package/rcf/user-stories/us-1203.json +43 -0
- package/rcf/user-stories/us-1204.json +25 -0
- package/releases/releases.yaml +21 -1
- package/src/blueprint/index.js +1 -0
- package/src/blueprint/remove-resolution.js +104 -0
- package/src/blueprint/supersede.js +2 -2
- package/src/cli/blueprint.js +41 -0
- package/src/cli/doctor.js +257 -5
- package/src/cli/init.js +166 -0
- package/src/setup/playwright-checks.js +426 -0
- package/src/verify/cli/run.js +28 -0
- package/src/verify/engine/index.js +24 -4
- package/src/verify/engine/launcher.js +41 -10
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// `rcf define blueprint remove-resolution <adr-id>` implementation.
|
|
2
|
+
//
|
|
3
|
+
// Removes a single entry from `manifest.resolutions[]` (and NOTHING
|
|
4
|
+
// else). The `<adr-id>` argument is matched against the entry's
|
|
5
|
+
// `resolvedByAdrId` field, which is how the doctor's probe-path-owner
|
|
6
|
+
// check (spec section 9) and the four-path resolution card (spec
|
|
7
|
+
// section 4) name the resolution to the operator.
|
|
8
|
+
//
|
|
9
|
+
// Behaviours (spec amendment A2, ratified 2026-09-04):
|
|
10
|
+
// - Removes the resolutions[] entry whose `resolvedByAdrId` equals
|
|
11
|
+
// the argument; leaves every other manifest section untouched. The
|
|
12
|
+
// project-level ADR file at `rcf/adrs/<adr-id>.json` is NOT
|
|
13
|
+
// deleted: an operator who wants to keep the ruling ADR as
|
|
14
|
+
// historical context after the redundant resolution goes away has
|
|
15
|
+
// that path open; an operator who wants the ADR gone can rm it
|
|
16
|
+
// themselves.
|
|
17
|
+
// - Refuses exit 2 when the argument is not a resolution entry on
|
|
18
|
+
// this manifest. Two flavours count as "not a resolution entry":
|
|
19
|
+
// the id is malformed (fails the ADR-\d{3,}(-<kebab-tail>)? grammar),
|
|
20
|
+
// or the id is well-formed but names no ADR anywhere on the
|
|
21
|
+
// tree AND is not present under any resolutions[] entry.
|
|
22
|
+
// - Idempotent on a second run: when the id is a well-formed ADR
|
|
23
|
+
// id that names an ADR present on the tree (the ruling ADR that
|
|
24
|
+
// the resolution had pointed at) but is not (any longer) present
|
|
25
|
+
// under any resolutions[] entry, the module returns
|
|
26
|
+
// `{ removed: false, alreadyAbsent: true }` and the CLI edge
|
|
27
|
+
// prints "nothing to remove" and exits 0. The distinction is:
|
|
28
|
+
// the ruling ADR still exists on disk, so the operator is running
|
|
29
|
+
// the SAME operation a second time, not typing a bogus id.
|
|
30
|
+
//
|
|
31
|
+
// The verb never touches project ADR files, blueprint records, or any
|
|
32
|
+
// other manifest section. That keeps the scope narrow enough that the
|
|
33
|
+
// operator can reason about the write without reading the module.
|
|
34
|
+
|
|
35
|
+
import { isRcfError, rcfError } from '../core/errors/index.js';
|
|
36
|
+
import { updateManifest } from './manifest-writer.js';
|
|
37
|
+
|
|
38
|
+
const ADR_ID_PATTERN = /^ADR-\d{3,}(?:-[a-z0-9]+(?:-[a-z0-9]+)*)?$/;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {object} RemoveResolutionResult
|
|
42
|
+
* @property {boolean} removed
|
|
43
|
+
* @property {boolean} [alreadyAbsent]
|
|
44
|
+
* @property {string} resolvedByAdrId
|
|
45
|
+
* @property {string} [resolutionId]
|
|
46
|
+
* @property {string} [topic]
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {object} args
|
|
51
|
+
* @param {string} args.projectRoot
|
|
52
|
+
* @param {import('#core/store/walker.js').TreeModel} args.tree
|
|
53
|
+
* @param {string} args.resolvedByAdrId
|
|
54
|
+
* @param {boolean} [args.dryRun]
|
|
55
|
+
* @returns {Promise<RemoveResolutionResult | import('../core/errors/index.js').RcfError>}
|
|
56
|
+
*/
|
|
57
|
+
export async function removeResolution({ projectRoot, tree, resolvedByAdrId, dryRun = false }) {
|
|
58
|
+
if (typeof resolvedByAdrId !== 'string' || resolvedByAdrId.trim().length === 0) {
|
|
59
|
+
return rcfError({ kind: 'usage', message: `<adr-id> is required (e.g. rcf define blueprint remove-resolution ADR-011-health-probes).` });
|
|
60
|
+
}
|
|
61
|
+
if (!ADR_ID_PATTERN.test(resolvedByAdrId)) {
|
|
62
|
+
return rcfError({ kind: 'usage', message: `'${resolvedByAdrId}' is not a well-formed ADR id (grammar: ADR-\\d{3,}(-<kebab-tail>)?).` });
|
|
63
|
+
}
|
|
64
|
+
const manifest = tree.manifest ?? {};
|
|
65
|
+
const resolutions = Array.isArray(manifest.resolutions) ? manifest.resolutions : [];
|
|
66
|
+
const index = resolutions.findIndex((r) => r?.resolvedByAdrId === resolvedByAdrId);
|
|
67
|
+
if (index === -1) {
|
|
68
|
+
// Not on resolutions[]. Two branches:
|
|
69
|
+
// - the ruling ADR file exists on the tree: idempotent no-op
|
|
70
|
+
// (this is a re-run of the same operation).
|
|
71
|
+
// - no ADR by that id anywhere: refuse. The operator has typed
|
|
72
|
+
// an id that this project has no record of, either as a
|
|
73
|
+
// resolution entry or as a project ADR.
|
|
74
|
+
const adrPresent = tree.byId instanceof Map && tree.byId.has(resolvedByAdrId);
|
|
75
|
+
if (adrPresent) {
|
|
76
|
+
return { removed: false, alreadyAbsent: true, resolvedByAdrId };
|
|
77
|
+
}
|
|
78
|
+
return rcfError({
|
|
79
|
+
kind: 'usage',
|
|
80
|
+
message: `'${resolvedByAdrId}' is not a resolution entry on this manifest (no resolutions[] record names it as resolvedByAdrId, and no ADR by that id exists on the project tree).`,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
const target = resolutions[index];
|
|
84
|
+
const result = await updateManifest({
|
|
85
|
+
projectRoot,
|
|
86
|
+
manifest,
|
|
87
|
+
mutate: (next) => {
|
|
88
|
+
const list = Array.isArray(next.resolutions) ? next.resolutions : [];
|
|
89
|
+
list.splice(index, 1);
|
|
90
|
+
// Drop the field entirely when empty so the manifest shape stays
|
|
91
|
+
// as compact as it was before any resolution was ever recorded.
|
|
92
|
+
if (list.length === 0) delete next.resolutions;
|
|
93
|
+
else next.resolutions = list;
|
|
94
|
+
},
|
|
95
|
+
dryRun,
|
|
96
|
+
});
|
|
97
|
+
if (isRcfError(result)) return result;
|
|
98
|
+
return {
|
|
99
|
+
removed: true,
|
|
100
|
+
resolvedByAdrId,
|
|
101
|
+
resolutionId: typeof target?.id === 'string' ? target.id : undefined,
|
|
102
|
+
topic: typeof target?.topic === 'string' ? target.topic : undefined,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
// the refused-add state; the escalation the worker adapted around was
|
|
27
27
|
// that AC-1002-5 could not pass as written. The `--incoming` argument
|
|
28
28
|
// was subsequently fed directly to `loadBlueprint`, which understands
|
|
29
|
-
// paths only
|
|
29
|
+
// paths only; a newcomer who intuited the `@stock/<slug>` form from
|
|
30
30
|
// `add`'s help hit a refusal. Persona re-run 2026-08-31 arc-4, H2.
|
|
31
31
|
//
|
|
32
32
|
// Two side effects, both governed by dryRun:
|
|
@@ -171,7 +171,7 @@ export async function supersedeBlueprintTopic({ projectRoot, tree, topic, incomi
|
|
|
171
171
|
// For library-qualified sources the applied identity is rewired
|
|
172
172
|
// under the library prefix (`<libraryPrefix>-<blueprintSlug>`), so
|
|
173
173
|
// the supersedes[] entry must reference that effective slug and
|
|
174
|
-
// stamp the ADR id under it
|
|
174
|
+
// stamp the ADR id under it; matching what `apply.js` writes to
|
|
175
175
|
// `manifest.blueprints[].slug`. For shelf / path sources the
|
|
176
176
|
// blueprint's own slug applies.
|
|
177
177
|
const effectiveSlug = resolved.kind === 'library'
|
package/src/cli/blueprint.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
groupRowsByCategory,
|
|
21
21
|
listBlueprints,
|
|
22
22
|
removeBlueprint,
|
|
23
|
+
removeResolution,
|
|
23
24
|
renderDiff,
|
|
24
25
|
resolveBlueprintSource,
|
|
25
26
|
supersedeBlueprintTopic,
|
|
@@ -66,6 +67,20 @@ Verbs:
|
|
|
66
67
|
diff <topic> Side-by-side view of every applied blueprint's
|
|
67
68
|
scope:global ADR on <topic>: id, path, title,
|
|
68
69
|
status, decision. Read-only.
|
|
70
|
+
remove-resolution <adr-id>
|
|
71
|
+
Remove a single manifest.resolutions[] entry by
|
|
72
|
+
its resolvedByAdrId (the id the doctor's
|
|
73
|
+
probe-path-owner check names when a historical
|
|
74
|
+
resolution has become redundant after a
|
|
75
|
+
blueprint upgrade). Writes nothing else: the
|
|
76
|
+
project-level ADR file at rcf/adrs/<adr-id>.json
|
|
77
|
+
is left in place as historical context. Refuses
|
|
78
|
+
exit 2 when <adr-id> is malformed or names no
|
|
79
|
+
ADR on the project tree. Idempotent on re-run:
|
|
80
|
+
when <adr-id> is a well-formed ADR id that
|
|
81
|
+
names an ADR present on the tree but is no
|
|
82
|
+
longer on any resolutions[] entry, prints
|
|
83
|
+
"nothing to remove" and exits 0.
|
|
69
84
|
library <verb> Manage external blueprint libraries. Sub-verbs:
|
|
70
85
|
add, list, remove, refresh. See
|
|
71
86
|
'rcf define blueprint library --help' for the
|
|
@@ -364,6 +379,32 @@ export async function main(argv, deps = {}) {
|
|
|
364
379
|
return 0;
|
|
365
380
|
}
|
|
366
381
|
|
|
382
|
+
if (verb === 'remove-resolution') {
|
|
383
|
+
if (rest.length === 0) {
|
|
384
|
+
stderr.write('[error] blueprint remove-resolution: missing <adr-id>\n');
|
|
385
|
+
return 2;
|
|
386
|
+
}
|
|
387
|
+
const adrId = rest[0];
|
|
388
|
+
const result = await removeResolution({
|
|
389
|
+
projectRoot, tree, resolvedByAdrId: adrId,
|
|
390
|
+
dryRun: parsed.values['dry-run'] === true,
|
|
391
|
+
});
|
|
392
|
+
if (isRcfError(result)) {
|
|
393
|
+
stderr.write(`[error] blueprint remove-resolution: ${result.message}\n`);
|
|
394
|
+
return 2;
|
|
395
|
+
}
|
|
396
|
+
if (result.alreadyAbsent) {
|
|
397
|
+
if (!parsed.values.quiet) stdout.write(`[blueprint] nothing to remove: '${adrId}' is not on manifest.resolutions[].\n`);
|
|
398
|
+
return 0;
|
|
399
|
+
}
|
|
400
|
+
if (!parsed.values.quiet) {
|
|
401
|
+
const topicSuffix = result.topic ? ` (topic '${result.topic}')` : '';
|
|
402
|
+
const idSuffix = result.resolutionId ? `; dropped resolution ${result.resolutionId}` : '';
|
|
403
|
+
stdout.write(`[blueprint] removed resolution for '${adrId}'${topicSuffix}${idSuffix}.\n`);
|
|
404
|
+
}
|
|
405
|
+
return 0;
|
|
406
|
+
}
|
|
407
|
+
|
|
367
408
|
stderr.write(`[error] blueprint: unknown verb '${verb}'\n`);
|
|
368
409
|
stderr.write(HELP);
|
|
369
410
|
return 2;
|
package/src/cli/doctor.js
CHANGED
|
@@ -47,6 +47,16 @@ import {
|
|
|
47
47
|
} from '../setup/managed-gitignore.js';
|
|
48
48
|
import { identityProfilePath } from '../setup/identity-seed.js';
|
|
49
49
|
import { knowledgePaths } from '../setup/knowledge-seed.js';
|
|
50
|
+
import {
|
|
51
|
+
checkBrowserPresent,
|
|
52
|
+
checkPlaywrightMcpReachable,
|
|
53
|
+
checkPlaywrightPresent,
|
|
54
|
+
findProjectPlaywrightKey,
|
|
55
|
+
FIX_LINES,
|
|
56
|
+
loadBrowserFacingSources,
|
|
57
|
+
probeClaudeCodeMcp,
|
|
58
|
+
SKIP_LINE_NON_BROWSER_FACING,
|
|
59
|
+
} from '../setup/playwright-checks.js';
|
|
50
60
|
|
|
51
61
|
const OPTION_SPEC = {
|
|
52
62
|
fix: { type: 'boolean' },
|
|
@@ -57,7 +67,25 @@ const OPTION_SPEC = {
|
|
|
57
67
|
help: { type: 'boolean' },
|
|
58
68
|
};
|
|
59
69
|
|
|
60
|
-
const KNOWN_CHECKS = /** @type {const} */ ([
|
|
70
|
+
const KNOWN_CHECKS = /** @type {const} */ ([
|
|
71
|
+
'agent-instructions',
|
|
72
|
+
'gitignore',
|
|
73
|
+
'knowledge',
|
|
74
|
+
'identity',
|
|
75
|
+
'playwright-present',
|
|
76
|
+
'browser-present',
|
|
77
|
+
'playwright-mcp-reachable',
|
|
78
|
+
'playwright-mcp-redundant',
|
|
79
|
+
'probe-path-owner',
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
/** The four Playwright-related checks doctor runs conditionally for
|
|
83
|
+
* browser-facing projects (spec 2026-09-03, section 3). */
|
|
84
|
+
const PLAYWRIGHT_CHECKS = /** @type {const} */ ([
|
|
85
|
+
'playwright-present',
|
|
86
|
+
'browser-present',
|
|
87
|
+
'playwright-mcp-reachable',
|
|
88
|
+
]);
|
|
61
89
|
|
|
62
90
|
export const HELP = `Usage: rcf doctor [--fix] [--check <check>[,check]] [--json] [--quiet] [--help]
|
|
63
91
|
|
|
@@ -73,8 +101,10 @@ Options:
|
|
|
73
101
|
removing the corrupted region.
|
|
74
102
|
--check <check>[,check] Run only the named checks. Default: all.
|
|
75
103
|
Values: agent-instructions, gitignore,
|
|
76
|
-
knowledge, identity
|
|
77
|
-
|
|
104
|
+
knowledge, identity, playwright-present,
|
|
105
|
+
browser-present, playwright-mcp-reachable,
|
|
106
|
+
playwright-mcp-redundant, probe-path-owner.
|
|
107
|
+
--json Emit machine-readable envelope: { ok, drift, writes, notices }.
|
|
78
108
|
--quiet Only summary line + first 3 drift items.
|
|
79
109
|
--force Accept a legacy-markers --fix on hand-edited
|
|
80
110
|
content that a non-interactive run would
|
|
@@ -155,12 +185,48 @@ export async function main(argv, deps = {}) {
|
|
|
155
185
|
force: Boolean(flags.force),
|
|
156
186
|
// isTty is deps-injectable for tests; falls back to real stdout.
|
|
157
187
|
isTty: deps.isTty ?? Boolean(stdout.isTTY),
|
|
188
|
+
// Doctor's Playwright-check seams (spec 2026-09-03, section 3). Every
|
|
189
|
+
// probe is injectable so the unit suite runs with none of these tools
|
|
190
|
+
// installed on the runner.
|
|
191
|
+
checkPlaywrightPresentImpl: deps.checkPlaywrightPresent ?? checkPlaywrightPresent,
|
|
192
|
+
checkBrowserPresentImpl: deps.checkBrowserPresent ?? checkBrowserPresent,
|
|
193
|
+
checkPlaywrightMcpReachableImpl:
|
|
194
|
+
deps.checkPlaywrightMcpReachable ?? checkPlaywrightMcpReachable,
|
|
195
|
+
probeClaudeCodeMcpImpl: deps.probeClaudeCodeMcp ?? probeClaudeCodeMcp,
|
|
196
|
+
loadBrowserFacingSourcesImpl:
|
|
197
|
+
deps.loadBrowserFacingSources ?? loadBrowserFacingSources,
|
|
198
|
+
readMcpJsonImpl: deps.readMcpJson ?? defaultReadMcpJson,
|
|
158
199
|
};
|
|
159
200
|
|
|
201
|
+
// Section 3.1: browser-facing projection. Computed once; every Playwright
|
|
202
|
+
// check reads the result from ctx rather than re-walking the manifest.
|
|
203
|
+
const browserFacingResult = await ctx.loadBrowserFacingSourcesImpl(cwd);
|
|
204
|
+
ctx.browserFacing = Boolean(browserFacingResult.browserFacing);
|
|
205
|
+
ctx.browserFacingSources = browserFacingResult.sources ?? [];
|
|
206
|
+
|
|
160
207
|
/** @type {Array<{check: string, item: string, file: string, message: string, refusedByFix: boolean}>} */
|
|
161
208
|
const drift = [];
|
|
162
209
|
/** @type {Array<{file: string, action: string}>} */
|
|
163
210
|
const writes = [];
|
|
211
|
+
/** @type {string[]} */
|
|
212
|
+
const notices = [];
|
|
213
|
+
|
|
214
|
+
// Section 3.4: skip line for the three Playwright checks on non-browser-
|
|
215
|
+
// facing projects. Emitted when at least one of the three is enabled but
|
|
216
|
+
// the project is not browser-facing AND the operator did not explicitly ask
|
|
217
|
+
// for that check by name (spec 3.5). We honour the --check filter by
|
|
218
|
+
// detecting whether the operator explicitly named any playwright check.
|
|
219
|
+
const explicitlyPickedPlaywrightChecks = new Set(
|
|
220
|
+
enabled.filter((c) => PLAYWRIGHT_CHECKS.includes(c)),
|
|
221
|
+
);
|
|
222
|
+
const anyPlaywrightCheckEnabled = explicitlyPickedPlaywrightChecks.size > 0;
|
|
223
|
+
const operatorAskedByName =
|
|
224
|
+
typeof flags.check === 'string'
|
|
225
|
+
&& flags.check.length > 0
|
|
226
|
+
&& explicitlyPickedPlaywrightChecks.size > 0;
|
|
227
|
+
if (anyPlaywrightCheckEnabled && !ctx.browserFacing && !operatorAskedByName) {
|
|
228
|
+
notices.push(SKIP_LINE_NON_BROWSER_FACING);
|
|
229
|
+
}
|
|
164
230
|
|
|
165
231
|
for (const check of enabled) {
|
|
166
232
|
let result;
|
|
@@ -168,7 +234,26 @@ export async function main(argv, deps = {}) {
|
|
|
168
234
|
else if (check === 'gitignore') result = await runGitignoreCheck(ctx);
|
|
169
235
|
else if (check === 'knowledge') result = await runKnowledgeCheck(ctx);
|
|
170
236
|
else if (check === 'identity') result = await runIdentityCheck(ctx);
|
|
171
|
-
else
|
|
237
|
+
else if (PLAYWRIGHT_CHECKS.includes(check)) {
|
|
238
|
+
// Skip the check on non-browser-facing projects unless the operator
|
|
239
|
+
// explicitly asked for this check by name (--check filter): the skip
|
|
240
|
+
// line above is the ground-truth diagnostic in that case (spec 3.4/3.5).
|
|
241
|
+
if (!ctx.browserFacing && !operatorAskedByName) continue;
|
|
242
|
+
if (check === 'playwright-present') result = await runPlaywrightPresentCheck(ctx);
|
|
243
|
+
else if (check === 'browser-present') result = await runBrowserPresentCheck(ctx);
|
|
244
|
+
else if (check === 'playwright-mcp-reachable') result = await runPlaywrightMcpReachableCheck(ctx);
|
|
245
|
+
else continue;
|
|
246
|
+
} else if (check === 'playwright-mcp-redundant') {
|
|
247
|
+
// Fires only on browser-facing projects (spec 4.5). Never runs on an
|
|
248
|
+
// API-only project even under an explicit --check ask.
|
|
249
|
+
if (!ctx.browserFacing) continue;
|
|
250
|
+
result = await runPlaywrightMcpRedundantCheck(ctx);
|
|
251
|
+
} else if (check === 'probe-path-owner') {
|
|
252
|
+
// Fires whenever more than one applied blueprint teaches probe paths.
|
|
253
|
+
// Runs on every project (not gated on browser-facing). Spec:
|
|
254
|
+
// projects/rcf-lite-wsd/specs/rcf-lite-probe-path-alignment-spec-2026-09-04.md section 9.
|
|
255
|
+
result = await runProbePathOwnerCheck(ctx);
|
|
256
|
+
} else continue;
|
|
172
257
|
for (const d of result.drift) drift.push({ check, ...d });
|
|
173
258
|
for (const w of result.writes) writes.push(w);
|
|
174
259
|
}
|
|
@@ -183,10 +268,15 @@ export async function main(argv, deps = {}) {
|
|
|
183
268
|
const exitCode = ok ? 0 : 3;
|
|
184
269
|
|
|
185
270
|
if (flags.json) {
|
|
186
|
-
stdout.write(`${JSON.stringify({ ok, drift, writes }, null, 2)}\n`);
|
|
271
|
+
stdout.write(`${JSON.stringify({ ok, drift, writes, notices }, null, 2)}\n`);
|
|
187
272
|
return exitCode;
|
|
188
273
|
}
|
|
189
274
|
|
|
275
|
+
// Notices (spec 3.4 skip line) are diagnostic ground truth. Emitted BEFORE
|
|
276
|
+
// the summary so an operator scanning the top of the output sees why the
|
|
277
|
+
// three checks did not fire on a non-browser-facing project. Not
|
|
278
|
+
// suppressed by --quiet.
|
|
279
|
+
for (const notice of notices) stdout.write(`${notice}\n`);
|
|
190
280
|
writeHumanSummary({ stdout, ok, drift, writes, fixed: ctx.fix, quiet: Boolean(flags.quiet) });
|
|
191
281
|
return exitCode;
|
|
192
282
|
}
|
|
@@ -546,3 +636,165 @@ async function isPathIgnored(projectRoot) {
|
|
|
546
636
|
|
|
547
637
|
// Silence unused-import warnings for values consumed only via names.
|
|
548
638
|
void composeGitignoreInner;
|
|
639
|
+
|
|
640
|
+
/* ------------------------------------------------------------------ */
|
|
641
|
+
/* Check: playwright-present (spec 3.3) */
|
|
642
|
+
/* ------------------------------------------------------------------ */
|
|
643
|
+
|
|
644
|
+
async function runPlaywrightPresentCheck(ctx) {
|
|
645
|
+
const drift = [];
|
|
646
|
+
const result = ctx.checkPlaywrightPresentImpl(ctx.projectRoot);
|
|
647
|
+
if (!result.ok) {
|
|
648
|
+
drift.push({
|
|
649
|
+
item: 'missing-peer',
|
|
650
|
+
file: 'package.json',
|
|
651
|
+
message: FIX_LINES['playwright-present'],
|
|
652
|
+
refusedByFix: true,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
return { drift, writes: [] };
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/* ------------------------------------------------------------------ */
|
|
659
|
+
/* Check: browser-present (spec 3.3) */
|
|
660
|
+
/* ------------------------------------------------------------------ */
|
|
661
|
+
|
|
662
|
+
async function runBrowserPresentCheck(ctx) {
|
|
663
|
+
const drift = [];
|
|
664
|
+
const result = await ctx.checkBrowserPresentImpl();
|
|
665
|
+
if (!result.ok) {
|
|
666
|
+
drift.push({
|
|
667
|
+
item: 'no-browser',
|
|
668
|
+
file: '(system)',
|
|
669
|
+
message: FIX_LINES['browser-present'],
|
|
670
|
+
refusedByFix: true,
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
return { drift, writes: [] };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/* ------------------------------------------------------------------ */
|
|
677
|
+
/* Check: playwright-mcp-reachable (spec 3.3) */
|
|
678
|
+
/* ------------------------------------------------------------------ */
|
|
679
|
+
|
|
680
|
+
async function runPlaywrightMcpReachableCheck(ctx) {
|
|
681
|
+
const drift = [];
|
|
682
|
+
const result = await ctx.checkPlaywrightMcpReachableImpl();
|
|
683
|
+
if (!result.ok) {
|
|
684
|
+
drift.push({
|
|
685
|
+
item: result.timedOut ? 'unreachable-timeout' : 'unreachable',
|
|
686
|
+
file: '(npx @playwright/mcp)',
|
|
687
|
+
message: FIX_LINES['playwright-mcp-reachable'],
|
|
688
|
+
refusedByFix: true,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
return { drift, writes: [] };
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/* ------------------------------------------------------------------ */
|
|
695
|
+
/* Check: playwright-mcp-redundant (spec 4.5) */
|
|
696
|
+
/* ------------------------------------------------------------------ */
|
|
697
|
+
|
|
698
|
+
async function runPlaywrightMcpRedundantCheck(ctx) {
|
|
699
|
+
const drift = [];
|
|
700
|
+
const mcpJson = await ctx.readMcpJsonImpl(ctx.projectRoot);
|
|
701
|
+
const projectKey = mcpJson ? findProjectPlaywrightKey(mcpJson) : null;
|
|
702
|
+
if (!projectKey) return { drift, writes: [] };
|
|
703
|
+
const probeResult = await ctx.probeClaudeCodeMcpImpl();
|
|
704
|
+
if (probeResult.kind !== 'found') return { drift, writes: [] };
|
|
705
|
+
drift.push({
|
|
706
|
+
item: 'redundant-entry',
|
|
707
|
+
file: '.mcp.json',
|
|
708
|
+
message: `project-scope .mcp.json carries a Playwright MCP entry ('${projectKey}') that is also declared at ${probeResult.scope} scope. The project entry shadows the user entry. Remove the project entry with \`rcf init --no-playwright-mcp\` (which re-runs init without writing it), or delete the '${projectKey}' entry from .mcp.json by hand.`,
|
|
709
|
+
refusedByFix: true,
|
|
710
|
+
});
|
|
711
|
+
return { drift, writes: [] };
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/* ------------------------------------------------------------------ */
|
|
715
|
+
/* Check: probe-path-owner (probe-path alignment spec section 9) */
|
|
716
|
+
/* ------------------------------------------------------------------ */
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Fires when more than one applied blueprint teaches probe paths. Reads
|
|
720
|
+
* `rcf/manifest.json` for the applied-blueprint records; counts how many
|
|
721
|
+
* carry a scope:global ADR on `healthProbes` (the shelf-wide probe-path
|
|
722
|
+
* ownership topic). More than one is drift: names the blueprints and the
|
|
723
|
+
* remedy line naming the one that would have to drop its opinion.
|
|
724
|
+
*
|
|
725
|
+
* Also names any resolutions[] entry the alignment removed as redundant
|
|
726
|
+
* (a project that applied essentials v1.x + probe-endpoints v1.0.0 and
|
|
727
|
+
* resolved the two topics via supersede will have a resolution entry
|
|
728
|
+
* pointing at both blueprint ADRs; after re-applying to essentials v2.0.0
|
|
729
|
+
* the essentials side no longer claims the topic and the resolution is
|
|
730
|
+
* redundant historical context the operator can remove).
|
|
731
|
+
*/
|
|
732
|
+
async function runProbePathOwnerCheck(ctx) {
|
|
733
|
+
const drift = [];
|
|
734
|
+
const manifestPath = join(ctx.projectRoot, 'rcf', 'manifest.json');
|
|
735
|
+
let manifest = null;
|
|
736
|
+
try {
|
|
737
|
+
manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
738
|
+
} catch { return { drift, writes: [] }; }
|
|
739
|
+
const applied = Array.isArray(manifest?.blueprints) ? manifest.blueprints : [];
|
|
740
|
+
const claimants = { healthProbes: [], readinessSemantics: [] };
|
|
741
|
+
for (const b of applied) {
|
|
742
|
+
for (const c of b?.contributions ?? []) {
|
|
743
|
+
if (c?.kind === 'adr' && c?.scope === 'global' && (c.topic === 'healthProbes' || c.topic === 'readinessSemantics')) {
|
|
744
|
+
claimants[c.topic].push({ slug: b.slug, adrId: c.id });
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
for (const topic of ['healthProbes', 'readinessSemantics']) {
|
|
749
|
+
if (claimants[topic].length > 1) {
|
|
750
|
+
const slugs = claimants[topic].map((x) => x.slug).join(', ');
|
|
751
|
+
const owner = claimants[topic].find((x) => x.slug === 'observability-probe-endpoints');
|
|
752
|
+
const remedy = owner
|
|
753
|
+
? `${claimants[topic].filter((x) => x.slug !== 'observability-probe-endpoints').map((x) => x.slug).join(', ')} would have to drop its scope:global claim on ${topic} (observability-probe-endpoints is the shelf canonical owner from probe-endpoints v1.0.0)`
|
|
754
|
+
: `one of ${slugs} would have to drop its scope:global claim on ${topic}`;
|
|
755
|
+
drift.push({
|
|
756
|
+
item: `multiple-${topic}-owners`,
|
|
757
|
+
file: 'rcf/manifest.json',
|
|
758
|
+
message: `more than one applied blueprint teaches probe paths on topic '${topic}': ${slugs}. ${remedy}. Spec: projects/rcf-lite-wsd/specs/rcf-lite-probe-path-alignment-spec-2026-09-04.md section 9.`,
|
|
759
|
+
refusedByFix: true,
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
// Redundant resolutions: a resolutions[] entry that names both
|
|
764
|
+
// `healthProbes` or `readinessSemantics` where the current claimant
|
|
765
|
+
// count is 1 (or 0) is redundant historical context.
|
|
766
|
+
const resolutions = Array.isArray(manifest?.resolutions) ? manifest.resolutions : [];
|
|
767
|
+
for (const r of resolutions) {
|
|
768
|
+
if (r?.topic === 'healthProbes' || r?.topic === 'readinessSemantics') {
|
|
769
|
+
if (claimants[r.topic].length <= 1) {
|
|
770
|
+
drift.push({
|
|
771
|
+
item: `redundant-${r.topic}-resolution`,
|
|
772
|
+
file: 'rcf/manifest.json',
|
|
773
|
+
message: `resolution '${r?.resolvedByAdrId ?? '(unnamed)'}' on topic '${r.topic}' is now redundant historical context: only ${claimants[r.topic].length} applied blueprint claims the topic after the probe-path alignment. Consider \`rcf define blueprint remove-resolution ${r?.resolvedByAdrId ?? '<id>'}\` or leave the resolution ADR as historical.`,
|
|
774
|
+
refusedByFix: true,
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
return { drift, writes: [] };
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Default reader for the project-root .mcp.json body. Returns the parsed
|
|
785
|
+
* object, or null on missing / unparseable (doctor treats an unparseable
|
|
786
|
+
* .mcp.json as "no signature findable" rather than a hard refusal here; the
|
|
787
|
+
* merge path in agent-setup already refuses unparseable with exit 2 on write).
|
|
788
|
+
*
|
|
789
|
+
* @param {string} projectRoot
|
|
790
|
+
* @returns {Promise<object|null>}
|
|
791
|
+
*/
|
|
792
|
+
async function defaultReadMcpJson(projectRoot) {
|
|
793
|
+
const file = join(projectRoot, '.mcp.json');
|
|
794
|
+
try {
|
|
795
|
+
const raw = await readFile(file, 'utf8');
|
|
796
|
+
return JSON.parse(raw);
|
|
797
|
+
} catch {
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
}
|