rcf-lite 0.0.1 → 0.8.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 +344 -0
- package/LICENSE +202 -0
- package/README.md +92 -4
- package/bin/rcf-verify.js +122 -0
- package/bin/rcf.js +174 -0
- package/bin/view-supervisor-child.mjs +14 -0
- package/fixtures/canary-manifest.json +103 -0
- package/guidance/README.md +25 -0
- package/guidance/build-cycle-playbook.md +492 -0
- package/guidance/build-cycle.md +40 -0
- package/guidance/document-model.md +45 -0
- package/guidance/elicitation-playbook.md +442 -0
- package/guidance/harness-template.md +246 -0
- package/guidance/managed/README.md +63 -0
- package/guidance/managed/agent-instructions-block.hash +1 -0
- package/guidance/managed/agent-instructions-block.md +220 -0
- package/guidance/managed/legacy-fragment-hashes.json +22 -0
- package/guidance/manifest.json +21 -0
- package/guidance/overview.md +48 -0
- package/guidance/persona-programme.md +44 -0
- package/package.json +71 -8
- package/rcf/adrs/adr-001.json +25 -0
- package/rcf/adrs/adr-002.json +25 -0
- package/rcf/adrs/adr-003.json +25 -0
- package/rcf/adrs/adr-004.json +20 -0
- package/rcf/adrs/adr-005.json +20 -0
- package/rcf/adrs/adr-006.json +25 -0
- package/rcf/adrs/adr-007.json +25 -0
- package/rcf/adrs/adr-008.json +25 -0
- package/rcf/adrs/adr-009.json +25 -0
- package/rcf/build-sequence.json +11 -0
- package/rcf/code-nodes/cn-001.json +14 -0
- package/rcf/code-nodes/cn-002.json +14 -0
- package/rcf/code-nodes/cn-003.json +15 -0
- package/rcf/code-nodes/cn-004.json +17 -0
- package/rcf/code-nodes/cn-005.json +19 -0
- package/rcf/code-nodes/cn-006.json +17 -0
- package/rcf/code-nodes/cn-007.json +16 -0
- package/rcf/code-nodes/cn-008.json +19 -0
- package/rcf/code-nodes/cn-009.json +16 -0
- package/rcf/code-nodes/cn-010.json +18 -0
- package/rcf/code-nodes/cn-011.json +18 -0
- package/rcf/code-nodes/cn-012.json +18 -0
- package/rcf/code-nodes/cn-013.json +19 -0
- package/rcf/code-nodes/cn-014.json +16 -0
- package/rcf/code-nodes/cn-015.json +16 -0
- package/rcf/code-nodes/cn-016.json +16 -0
- package/rcf/code-nodes/cn-017.json +16 -0
- package/rcf/code-nodes/cn-018.json +15 -0
- package/rcf/code-nodes/cn-019.json +16 -0
- package/rcf/code-nodes/cn-020.json +16 -0
- package/rcf/code-nodes/cn-021.json +16 -0
- package/rcf/code-nodes/cn-022.json +16 -0
- package/rcf/code-nodes/cn-023.json +16 -0
- package/rcf/code-nodes/cn-024.json +16 -0
- package/rcf/code-nodes/cn-025.json +16 -0
- package/rcf/code-nodes/cn-026.json +16 -0
- package/rcf/code-nodes/cn-027.json +16 -0
- package/rcf/code-nodes/cn-028.json +16 -0
- package/rcf/code-nodes/cn-029.json +19 -0
- package/rcf/code-nodes/cn-030.json +14 -0
- package/rcf/code-nodes/cn-031.json +14 -0
- package/rcf/code-nodes/cn-032.json +14 -0
- package/rcf/code-nodes/cn-033.json +14 -0
- package/rcf/code-nodes/cn-034.json +14 -0
- package/rcf/code-nodes/cn-035.json +14 -0
- package/rcf/code-nodes/cn-036.json +14 -0
- package/rcf/code-nodes/cn-037.json +14 -0
- package/rcf/code-nodes/cn-038.json +14 -0
- package/rcf/code-nodes/cn-039.json +14 -0
- package/rcf/code-nodes/cn-040.json +14 -0
- package/rcf/code-nodes/cn-041.json +14 -0
- package/rcf/code-nodes/cn-042.json +14 -0
- package/rcf/code-nodes/cn-043.json +14 -0
- package/rcf/code-nodes/cn-044.json +14 -0
- package/rcf/code-nodes/cn-045.json +14 -0
- package/rcf/code-nodes/cn-046.json +14 -0
- package/rcf/code-nodes/cn-047.json +14 -0
- package/rcf/code-nodes/cn-048.json +14 -0
- package/rcf/code-nodes/cn-049.json +14 -0
- package/rcf/code-nodes/cn-050.json +14 -0
- package/rcf/code-nodes/cn-051.json +14 -0
- package/rcf/code-nodes/cn-052.json +14 -0
- package/rcf/code-nodes/cn-053.json +14 -0
- package/rcf/code-nodes/cn-054.json +14 -0
- package/rcf/code-nodes/cn-055.json +14 -0
- package/rcf/code-nodes/cn-056.json +14 -0
- package/rcf/code-nodes/cn-057.json +14 -0
- package/rcf/fbs/fbs-001.json +49 -0
- package/rcf/fbs/fbs-002.json +42 -0
- package/rcf/fbs/fbs-003.json +37 -0
- package/rcf/fbs/fbs-004.json +39 -0
- package/rcf/fbs/fbs-005.json +38 -0
- package/rcf/fbs/fbs-006.json +48 -0
- package/rcf/fbs/fbs-007.json +39 -0
- package/rcf/fbs/fbs-008.json +40 -0
- package/rcf/fbs/fbs-009.json +36 -0
- package/rcf/fbs/fbs-010.json +41 -0
- package/rcf/fbs/fbs-011.json +36 -0
- package/rcf/fbs/fbs-012.json +46 -0
- package/rcf/fbs/fbs-013.json +42 -0
- package/rcf/fbs/fbs-014.json +49 -0
- package/rcf/fbs/fbs-015.json +32 -0
- package/rcf/manifest.json +17 -0
- package/rcf/prd.json +47 -0
- package/rcf/requirements/req-001.json +19 -0
- package/rcf/requirements/req-002.json +19 -0
- package/rcf/requirements/req-003.json +19 -0
- package/rcf/requirements/req-004.json +19 -0
- package/rcf/requirements/req-005.json +19 -0
- package/rcf/requirements/req-006.json +19 -0
- package/rcf/requirements/req-007.json +19 -0
- package/rcf/requirements/req-008.json +19 -0
- package/rcf/requirements/req-009.json +19 -0
- package/rcf/tacs/tac-001.json +45 -0
- package/rcf/tacs/tac-002.json +109 -0
- package/rcf/tacs/tac-003.json +40 -0
- package/rcf/tacs/tac-004.json +51 -0
- package/rcf/tacs/tac-005.json +52 -0
- package/rcf/tacs/tac-006.json +104 -0
- package/rcf/tacs/tac-007.json +38 -0
- package/rcf/tacs/tac-008.json +51 -0
- package/rcf/tad.json +51 -0
- package/rcf/test-suites/PENDING.md +23 -0
- package/rcf/test-suites/ts-001.json +38 -0
- package/rcf/test-suites/ts-002.json +38 -0
- package/rcf/test-suites/ts-003.json +43 -0
- package/rcf/test-suites/ts-004.json +44 -0
- package/rcf/test-suites/ts-005.json +30 -0
- package/rcf/test-suites/ts-006.json +36 -0
- package/rcf/test-suites/ts-007.json +43 -0
- package/rcf/test-suites/ts-008.json +37 -0
- package/rcf/test-suites/ts-009.json +38 -0
- package/rcf/test-suites/ts-010.json +38 -0
- package/rcf/test-suites/ts-011.json +44 -0
- package/rcf/test-suites/ts-012.json +36 -0
- package/rcf/test-suites/ts-013.json +38 -0
- package/rcf/test-suites/ts-014.json +38 -0
- package/rcf/test-suites/ts-015.json +38 -0
- package/rcf/test-suites/ts-016.json +37 -0
- package/rcf/test-suites/ts-017.json +38 -0
- package/rcf/test-suites/ts-018.json +38 -0
- package/rcf/test-suites/ts-019.json +37 -0
- package/rcf/test-suites/ts-020.json +46 -0
- package/rcf/test-suites/ts-021.json +46 -0
- package/rcf/test-suites/ts-022.json +46 -0
- package/rcf/test-suites/ts-023.json +46 -0
- package/rcf/test-suites/ts-024.json +46 -0
- package/rcf/test-suites/ts-025.json +52 -0
- package/rcf/user-stories/us-101.json +40 -0
- package/rcf/user-stories/us-102.json +40 -0
- package/rcf/user-stories/us-201.json +40 -0
- package/rcf/user-stories/us-202.json +40 -0
- package/rcf/user-stories/us-203.json +32 -0
- package/rcf/user-stories/us-301.json +40 -0
- package/rcf/user-stories/us-302.json +40 -0
- package/rcf/user-stories/us-303.json +40 -0
- package/rcf/user-stories/us-304.json +40 -0
- package/rcf/user-stories/us-401.json +40 -0
- package/rcf/user-stories/us-402.json +40 -0
- package/rcf/user-stories/us-403.json +40 -0
- package/rcf/user-stories/us-501.json +40 -0
- package/rcf/user-stories/us-502.json +40 -0
- package/rcf/user-stories/us-503.json +40 -0
- package/rcf/user-stories/us-601.json +40 -0
- package/rcf/user-stories/us-602.json +40 -0
- package/rcf/user-stories/us-701.json +40 -0
- package/rcf/user-stories/us-702.json +40 -0
- package/rcf/user-stories/us-801.json +49 -0
- package/rcf/user-stories/us-802.json +49 -0
- package/rcf/user-stories/us-803.json +49 -0
- package/rcf/user-stories/us-804.json +49 -0
- package/rcf/user-stories/us-805.json +49 -0
- package/rcf/user-stories/us-901.json +40 -0
- package/src/.gitkeep +0 -0
- package/src/admissibility/enforce.js +142 -0
- package/src/admissibility/index.js +8 -0
- package/src/admissibility/markers.js +104 -0
- package/src/admissibility/scope-lint.js +163 -0
- package/src/browser-verify/auth-smoke.js +109 -0
- package/src/browser-verify/index.js +29 -0
- package/src/browser-verify/invariants.js +336 -0
- package/src/browser-verify/manifest-writer.js +189 -0
- package/src/browser-verify/runner.js +171 -0
- package/src/build/bundle.js +198 -0
- package/src/build/formatters/json.js +18 -0
- package/src/build/formatters/markdown.js +406 -0
- package/src/build/index.js +14 -0
- package/src/build/mark.js +177 -0
- package/src/build/queue.js +285 -0
- package/src/cli/browser-verify.js +231 -0
- package/src/cli/build.js +584 -0
- package/src/cli/coverage.js +219 -0
- package/src/cli/create.js +358 -0
- package/src/cli/delete.js +127 -0
- package/src/cli/design.js +353 -0
- package/src/cli/doctor.js +548 -0
- package/src/cli/fbs.js +171 -0
- package/src/cli/finalise.js +377 -0
- package/src/cli/guidance.js +206 -0
- package/src/cli/help.js +156 -0
- package/src/cli/impact.js +119 -0
- package/src/cli/init.js +282 -0
- package/src/cli/intake.js +153 -0
- package/src/cli/link.js +128 -0
- package/src/cli/mcp.js +160 -0
- package/src/cli/preflight.js +220 -0
- package/src/cli/read.js +168 -0
- package/src/cli/req-baseline.js +269 -0
- package/src/cli/req-classify.js +135 -0
- package/src/cli/review.js +295 -0
- package/src/cli/test-suite.js +221 -0
- package/src/cli/trace.js +172 -0
- package/src/cli/ui-baseline.js +292 -0
- package/src/cli/ui-classify.js +108 -0
- package/src/cli/update.js +197 -0
- package/src/cli/validate.js +168 -0
- package/src/cli/view.js +452 -0
- package/src/core/baseline-catalog/data/auth.json +42 -0
- package/src/core/baseline-catalog/data/http-api.json +42 -0
- package/src/core/baseline-catalog/data/notifications.json +33 -0
- package/src/core/baseline-catalog/data/persistence.json +33 -0
- package/src/core/baseline-catalog/data/web-ui.json +60 -0
- package/src/core/baseline-catalog/index.js +121 -0
- package/src/core/errors/index.js +167 -0
- package/src/core/fixtures/register-canary/canary-prompt-01.json +18 -0
- package/src/core/fixtures/register-canary/canary-prompt-02.json +21 -0
- package/src/core/fixtures/register-canary/canary-prompt-03.json +17 -0
- package/src/core/isolation/index.js +60 -0
- package/src/core/mcp/framing.js +103 -0
- package/src/core/mcp/index.js +8 -0
- package/src/core/mcp/server.js +228 -0
- package/src/core/patterns/register-canary.js +209 -0
- package/src/core/patterns/req-shapes.js +158 -0
- package/src/core/patterns/services.js +358 -0
- package/src/core/patterns/ui-shapes.js +166 -0
- package/src/core/store/cn-resolve.js +134 -0
- package/src/core/store/derive-deps.js +93 -0
- package/src/core/store/ids.js +78 -0
- package/src/core/store/index.js +20 -0
- package/src/core/store/init.js +255 -0
- package/src/core/store/loader.js +214 -0
- package/src/core/store/tp-resolve.js +176 -0
- package/src/core/store/validator.js +191 -0
- package/src/core/store/walker.js +944 -0
- package/src/core/store/writer.js +1879 -0
- package/src/design/index.js +11 -0
- package/src/design/writer.js +271 -0
- package/src/finalise/detect.js +151 -0
- package/src/finalise/index.js +31 -0
- package/src/finalise/ingest.js +160 -0
- package/src/finalise/install.js +119 -0
- package/src/finalise/ship-without-verified.js +131 -0
- package/src/finalise/spawn.js +84 -0
- package/src/intake/fidelity.js +105 -0
- package/src/intake/index.js +6 -0
- package/src/intake/manifest-writer.js +100 -0
- package/src/intake/orchestrator.js +138 -0
- package/src/intake/validate.js +80 -0
- package/src/mcp/map-errors.js +131 -0
- package/src/mcp/prompts.js +49 -0
- package/src/mcp/resources.js +244 -0
- package/src/mcp/tools.js +1212 -0
- package/src/preflight/design-shapes.js +185 -0
- package/src/preflight/index.js +90 -0
- package/src/preflight/manifest-writer.js +264 -0
- package/src/preflight/scanner.js +206 -0
- package/src/preflight/secrets.js +134 -0
- package/src/preflight/session.js +246 -0
- package/src/query/attestation.js +285 -0
- package/src/query/coverage.js +308 -0
- package/src/query/formatters/json.js +21 -0
- package/src/query/formatters/mermaid.js +209 -0
- package/src/query/formatters/table.js +203 -0
- package/src/query/impact.js +173 -0
- package/src/query/index.js +13 -0
- package/src/query/refuse-on-admissibility.js +73 -0
- package/src/query/trace.js +345 -0
- package/src/register-canary/fixture-loader.js +87 -0
- package/src/register-canary/index.js +10 -0
- package/src/register-canary/record-writer.js +132 -0
- package/src/register-canary/runner.js +156 -0
- package/src/req-baseline/gate.js +86 -0
- package/src/req-baseline/index.js +27 -0
- package/src/req-baseline/open-candidates.js +143 -0
- package/src/req-baseline/opt-out.js +195 -0
- package/src/req-baseline/sweep.js +230 -0
- package/src/req-detection/classifier.js +181 -0
- package/src/req-detection/index.js +9 -0
- package/src/req-detection/persist.js +55 -0
- package/src/review/index.js +325 -0
- package/src/review/mutation.js +117 -0
- package/src/review/ui-baseline-drift.js +138 -0
- package/src/ruleset/index.js +140 -0
- package/src/ruleset/ruleset.json +146 -0
- package/src/server/index.js +178 -0
- package/src/server/routes.js +110 -0
- package/src/server/sse.js +118 -0
- package/src/setup/agent-setup.js +362 -0
- package/src/setup/identity-seed.js +104 -0
- package/src/setup/knowledge-seed.js +123 -0
- package/src/setup/managed-block.js +193 -0
- package/src/setup/managed-gitignore.js +166 -0
- package/src/setup/managed-markers.js +49 -0
- package/src/ui-baseline/defaults.js +119 -0
- package/src/ui-baseline/index.js +25 -0
- package/src/ui-baseline/manifest-writer.js +282 -0
- package/src/ui-baseline/session.js +178 -0
- package/src/ui-detection/classifier.js +192 -0
- package/src/verify/chain/index.js +221 -0
- package/src/verify/cli/cleanup.js +61 -0
- package/src/verify/cli/help.js +56 -0
- package/src/verify/cli/mcp.js +98 -0
- package/src/verify/cli/provision.js +71 -0
- package/src/verify/cli/report.js +71 -0
- package/src/verify/cli/run.js +155 -0
- package/src/verify/engine/brief.js +87 -0
- package/src/verify/engine/index.js +177 -0
- package/src/verify/engine/launcher.js +307 -0
- package/src/verify/mcp/tools.js +107 -0
- package/src/verify/profile/index.js +146 -0
- package/src/verify/provision/index.js +256 -0
- package/src/verify/report/index.js +139 -0
- package/src/verify/report/renderer.js +118 -0
- package/src/verify/verdict/index.js +313 -0
- package/src/view/doc-renderers/adr.js +44 -0
- package/src/view/doc-renderers/build-sequence.js +40 -0
- package/src/view/doc-renderers/fbs.js +128 -0
- package/src/view/doc-renderers/helpers.js +159 -0
- package/src/view/doc-renderers/index.js +12 -0
- package/src/view/doc-renderers/prd.js +45 -0
- package/src/view/doc-renderers/req.js +43 -0
- package/src/view/doc-renderers/tac.js +38 -0
- package/src/view/doc-renderers/tad.js +74 -0
- package/src/view/doc-renderers/test-suite.js +45 -0
- package/src/view/doc-renderers/user-story.js +63 -0
- package/src/view/html-page.js +462 -0
- package/src/view/index.js +63 -0
- package/src/view/live-client.js +338 -0
- package/src/view/mermaid-diagram.js +178 -0
- package/src/view/style.css +735 -0
- package/src/view/tree-model.js +152 -0
- package/src/view/vendored/mermaid.min.js +2607 -0
- package/src/view-supervisor/index.js +26 -0
- package/src/view-supervisor/logs.js +32 -0
- package/src/view-supervisor/manifest-writer.js +178 -0
- package/src/view-supervisor/persist-until.js +85 -0
- package/src/view-supervisor/supervisor.js +276 -0
- package/src/watch/index.js +152 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
// `rcf doctor` subcommand handler (0.6.0 spec §2.2). Diagnoses
|
|
2
|
+
// init-hygiene drift across four checks (agent-instructions, gitignore,
|
|
3
|
+
// knowledge, identity) and, with --fix, applies the safe minimal
|
|
4
|
+
// repair. Never runs implicitly: no init hook, no post-install script,
|
|
5
|
+
// no validate sub-call fires --fix on the operator's behalf (§2.8).
|
|
6
|
+
//
|
|
7
|
+
// Exit codes:
|
|
8
|
+
// - 0 clean (every check enabled reports clean).
|
|
9
|
+
// - 3 drift (at least one check reports drift).
|
|
10
|
+
// - 2 usage error.
|
|
11
|
+
//
|
|
12
|
+
// --fix contract (§2.7): rewrites managed blocks WHOLESALE inside the
|
|
13
|
+
// markers; every byte outside the markers is preserved verbatim. Files
|
|
14
|
+
// with structurally broken markers (orphan / duplicate) are refused;
|
|
15
|
+
// the operator repairs by hand. Running --fix on a clean repo writes
|
|
16
|
+
// zero files (idempotent no-op).
|
|
17
|
+
|
|
18
|
+
import { readFile, stat, writeFile } from 'node:fs/promises';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { parseArgs } from 'node:util';
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
loadLegacyFragmentHashes,
|
|
24
|
+
loadManagedBlock,
|
|
25
|
+
loadManagedBlockHash,
|
|
26
|
+
managedBlockPath,
|
|
27
|
+
} from '../setup/agent-setup.js';
|
|
28
|
+
import {
|
|
29
|
+
MARKER_BEGIN,
|
|
30
|
+
MARKER_END,
|
|
31
|
+
LEGACY_MARKER_BEGIN,
|
|
32
|
+
LEGACY_MARKER_END,
|
|
33
|
+
} from '../setup/managed-markers.js';
|
|
34
|
+
import {
|
|
35
|
+
applyFix as applyManagedBlockFix,
|
|
36
|
+
classifyBlock,
|
|
37
|
+
hashInnerContent,
|
|
38
|
+
} from '../setup/managed-block.js';
|
|
39
|
+
import {
|
|
40
|
+
composeGitignoreBlock,
|
|
41
|
+
composeGitignoreInner,
|
|
42
|
+
computeGitignoreBlockHash,
|
|
43
|
+
extractGitignoreBlock,
|
|
44
|
+
GITIGNORE_MARKER_BEGIN,
|
|
45
|
+
GITIGNORE_MARKER_END,
|
|
46
|
+
managedGitignoreEntries,
|
|
47
|
+
} from '../setup/managed-gitignore.js';
|
|
48
|
+
import { identityProfilePath } from '../setup/identity-seed.js';
|
|
49
|
+
import { knowledgePaths } from '../setup/knowledge-seed.js';
|
|
50
|
+
|
|
51
|
+
const OPTION_SPEC = {
|
|
52
|
+
fix: { type: 'boolean' },
|
|
53
|
+
check: { type: 'string' },
|
|
54
|
+
json: { type: 'boolean' },
|
|
55
|
+
quiet: { type: 'boolean' },
|
|
56
|
+
force: { type: 'boolean' },
|
|
57
|
+
help: { type: 'boolean' },
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const KNOWN_CHECKS = /** @type {const} */ (['agent-instructions', 'gitignore', 'knowledge', 'identity']);
|
|
61
|
+
|
|
62
|
+
export const HELP = `Usage: rcf doctor [--fix] [--check <check>[,check]] [--json] [--quiet] [--help]
|
|
63
|
+
|
|
64
|
+
Diagnose init-hygiene drift in the current project. Exits 0 clean, 3 when
|
|
65
|
+
any check reports drift, 2 on usage errors.
|
|
66
|
+
|
|
67
|
+
Options:
|
|
68
|
+
--fix Apply the minimal safe repair for every check
|
|
69
|
+
that reports drift. Rewrites managed blocks
|
|
70
|
+
wholesale; leaves operator content untouched.
|
|
71
|
+
Refuses to touch files with structurally broken
|
|
72
|
+
markers; those must be resolved by hand or by
|
|
73
|
+
removing the corrupted region.
|
|
74
|
+
--check <check>[,check] Run only the named checks. Default: all.
|
|
75
|
+
Values: agent-instructions, gitignore,
|
|
76
|
+
knowledge, identity.
|
|
77
|
+
--json Emit machine-readable envelope: { ok, drift[] }.
|
|
78
|
+
--quiet Only summary line + first 3 drift items.
|
|
79
|
+
--force Accept a legacy-markers --fix on hand-edited
|
|
80
|
+
content that a non-interactive run would
|
|
81
|
+
otherwise refuse. See rcf help doctor for the
|
|
82
|
+
hand-edited-legacy migration rules.
|
|
83
|
+
--help Print this help.
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {string[]} argv - argv slice after `doctor`
|
|
88
|
+
* @param {object} [deps]
|
|
89
|
+
* @returns {Promise<number>}
|
|
90
|
+
*/
|
|
91
|
+
export async function main(argv, deps = {}) {
|
|
92
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
93
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
94
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
95
|
+
|
|
96
|
+
let parsed;
|
|
97
|
+
try {
|
|
98
|
+
parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: true, strict: true });
|
|
99
|
+
} catch (err) {
|
|
100
|
+
stderr.write(`[error] usage ${err.message}\n`);
|
|
101
|
+
stderr.write(HELP);
|
|
102
|
+
return 2;
|
|
103
|
+
}
|
|
104
|
+
const flags = parsed.values;
|
|
105
|
+
if (flags.help) {
|
|
106
|
+
stdout.write(HELP);
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
if (parsed.positionals.length > 0) {
|
|
110
|
+
stderr.write(`[error] usage doctor: unexpected argument '${parsed.positionals[0]}'\n`);
|
|
111
|
+
stderr.write(HELP);
|
|
112
|
+
return 2;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let enabled = [...KNOWN_CHECKS];
|
|
116
|
+
if (typeof flags.check === 'string' && flags.check.length > 0) {
|
|
117
|
+
const asked = flags.check.split(',').map((s) => s.trim()).filter((s) => s.length > 0);
|
|
118
|
+
const bad = asked.filter((c) => !KNOWN_CHECKS.includes(/** @type {(typeof KNOWN_CHECKS)[number]} */ (c)));
|
|
119
|
+
if (bad.length > 0) {
|
|
120
|
+
stderr.write(`[error] usage doctor: unknown check '${bad[0]}'. Known checks: ${KNOWN_CHECKS.join(', ')}\n`);
|
|
121
|
+
return 2;
|
|
122
|
+
}
|
|
123
|
+
enabled = asked;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Load canonical block + hash once; a missing shipped asset is a
|
|
127
|
+
// distinct error class so doctor never reports spurious clean.
|
|
128
|
+
const canonical = await loadManagedBlock();
|
|
129
|
+
if (typeof canonical !== 'string') {
|
|
130
|
+
stderr.write(`[error] ${canonical.kind} ${canonical.message}\n`);
|
|
131
|
+
return 1;
|
|
132
|
+
}
|
|
133
|
+
const canonicalHash = await loadManagedBlockHash();
|
|
134
|
+
if (typeof canonicalHash !== 'string') {
|
|
135
|
+
stderr.write(`[error] ${canonicalHash.kind} ${canonicalHash.message}\n`);
|
|
136
|
+
return 1;
|
|
137
|
+
}
|
|
138
|
+
// §7.3 fail-safe hand-edit detector: any legacy inner content whose
|
|
139
|
+
// hash is NOT in this whitelist is treated as hand-edited (warn on
|
|
140
|
+
// TTY, refuse without --force on non-TTY). A missing / malformed
|
|
141
|
+
// whitelist is a distinct error class so doctor never quietly falls
|
|
142
|
+
// back to a permissive heuristic.
|
|
143
|
+
const legacyFragmentHashes = await loadLegacyFragmentHashes();
|
|
144
|
+
if (!(legacyFragmentHashes instanceof Set)) {
|
|
145
|
+
stderr.write(`[error] ${legacyFragmentHashes.kind} ${legacyFragmentHashes.message}\n`);
|
|
146
|
+
return 1;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const ctx = {
|
|
150
|
+
projectRoot: cwd,
|
|
151
|
+
canonical,
|
|
152
|
+
canonicalHash,
|
|
153
|
+
legacyFragmentHashes,
|
|
154
|
+
fix: Boolean(flags.fix),
|
|
155
|
+
force: Boolean(flags.force),
|
|
156
|
+
// isTty is deps-injectable for tests; falls back to real stdout.
|
|
157
|
+
isTty: deps.isTty ?? Boolean(stdout.isTTY),
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/** @type {Array<{check: string, item: string, file: string, message: string, refusedByFix: boolean}>} */
|
|
161
|
+
const drift = [];
|
|
162
|
+
/** @type {Array<{file: string, action: string}>} */
|
|
163
|
+
const writes = [];
|
|
164
|
+
|
|
165
|
+
for (const check of enabled) {
|
|
166
|
+
let result;
|
|
167
|
+
if (check === 'agent-instructions') result = await runAgentInstructionsCheck(ctx);
|
|
168
|
+
else if (check === 'gitignore') result = await runGitignoreCheck(ctx);
|
|
169
|
+
else if (check === 'knowledge') result = await runKnowledgeCheck(ctx);
|
|
170
|
+
else if (check === 'identity') result = await runIdentityCheck(ctx);
|
|
171
|
+
else continue;
|
|
172
|
+
for (const d of result.drift) drift.push({ check, ...d });
|
|
173
|
+
for (const w of result.writes) writes.push(w);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// §2.7 exit-code semantics: with --fix, a run that repaired every
|
|
177
|
+
// fixable drift item exits 0 even though drift WAS reported at scan
|
|
178
|
+
// time. Exit 3 only when at least one drift item remains unrepaired
|
|
179
|
+
// (either --fix was not passed, or the item was refused by --fix).
|
|
180
|
+
const refusedCount = drift.filter((d) => d.refusedByFix).length;
|
|
181
|
+
const unrepairedCount = ctx.fix ? refusedCount : drift.length;
|
|
182
|
+
const ok = unrepairedCount === 0;
|
|
183
|
+
const exitCode = ok ? 0 : 3;
|
|
184
|
+
|
|
185
|
+
if (flags.json) {
|
|
186
|
+
stdout.write(`${JSON.stringify({ ok, drift, writes }, null, 2)}\n`);
|
|
187
|
+
return exitCode;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
writeHumanSummary({ stdout, ok, drift, writes, fixed: ctx.fix, quiet: Boolean(flags.quiet) });
|
|
191
|
+
return exitCode;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Render a human-readable summary. */
|
|
195
|
+
function writeHumanSummary({ stdout, ok, drift, writes, fixed, quiet }) {
|
|
196
|
+
const repaired = writes.length;
|
|
197
|
+
if (ok && repaired === 0) {
|
|
198
|
+
stdout.write('rcf doctor: clean.\n');
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (ok && repaired > 0) {
|
|
202
|
+
// Every drift item was repaired successfully. Report the repair
|
|
203
|
+
// count so the operator sees what the run did.
|
|
204
|
+
stdout.write(`rcf doctor: ${repaired} item${repaired === 1 ? '' : 's'} repaired; clean.\n`);
|
|
205
|
+
for (const w of writes) stdout.write(` fixed: ${w.file} (${w.action}).\n`);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const refused = drift.filter((d) => d.refusedByFix);
|
|
209
|
+
stdout.write(`rcf doctor: ${drift.length} drift item${drift.length === 1 ? '' : 's'}`);
|
|
210
|
+
if (fixed) stdout.write(` (${repaired} repaired, ${refused.length} refused)`);
|
|
211
|
+
stdout.write('.\n');
|
|
212
|
+
const limit = quiet ? 3 : drift.length;
|
|
213
|
+
for (const d of drift.slice(0, limit)) {
|
|
214
|
+
const tag = d.refusedByFix ? ' [refused]' : '';
|
|
215
|
+
stdout.write(` [${d.check}] ${d.item} ${d.file}${tag}\n`);
|
|
216
|
+
if (!quiet) stdout.write(` ${d.message}\n`);
|
|
217
|
+
}
|
|
218
|
+
if (limit < drift.length) {
|
|
219
|
+
stdout.write(` ... ${drift.length - limit} more (run without --quiet to see all).\n`);
|
|
220
|
+
}
|
|
221
|
+
if (fixed) {
|
|
222
|
+
for (const w of writes) stdout.write(` fixed: ${w.file} (${w.action}).\n`);
|
|
223
|
+
} else {
|
|
224
|
+
stdout.write('Run `rcf doctor --fix` to apply the safe repairs above.\n');
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* ------------------------------------------------------------------ */
|
|
229
|
+
/* Check: agent-instructions */
|
|
230
|
+
/* ------------------------------------------------------------------ */
|
|
231
|
+
|
|
232
|
+
async function runAgentInstructionsCheck(ctx) {
|
|
233
|
+
const drift = [];
|
|
234
|
+
const writes = [];
|
|
235
|
+
const blockOpts = {
|
|
236
|
+
markerBegin: MARKER_BEGIN,
|
|
237
|
+
markerEnd: MARKER_END,
|
|
238
|
+
legacyMarkerBegin: LEGACY_MARKER_BEGIN,
|
|
239
|
+
legacyMarkerEnd: LEGACY_MARKER_END,
|
|
240
|
+
};
|
|
241
|
+
for (const name of ['CLAUDE.md', 'AGENTS.md']) {
|
|
242
|
+
const path = join(ctx.projectRoot, name);
|
|
243
|
+
let text;
|
|
244
|
+
try {
|
|
245
|
+
text = await readFile(path, 'utf8');
|
|
246
|
+
} catch (err) {
|
|
247
|
+
if (/** @type {NodeJS.ErrnoException} */ (err).code === 'ENOENT') continue;
|
|
248
|
+
throw err;
|
|
249
|
+
}
|
|
250
|
+
const state = classifyBlock(text, ctx.canonicalHash, blockOpts);
|
|
251
|
+
if (state === 'clean') continue;
|
|
252
|
+
const refusedByFix = state === 'orphan-marker' || state === 'duplicate-block';
|
|
253
|
+
const item = state;
|
|
254
|
+
const message = messageForState(state, path);
|
|
255
|
+
drift.push({ item, file: name, message, refusedByFix });
|
|
256
|
+
if (ctx.fix && !refusedByFix) {
|
|
257
|
+
if (state === 'legacy-markers' && !ctx.force) {
|
|
258
|
+
// Hand-edited-legacy migration warning (§7.3). Non-interactive
|
|
259
|
+
// runs REFUSE without --force to avoid destroying operator edits.
|
|
260
|
+
const legacyHasHandEdits = detectLegacyHandEdits(text, ctx.legacyFragmentHashes);
|
|
261
|
+
if (legacyHasHandEdits && !ctx.isTty) {
|
|
262
|
+
// Convert the drift into a refused item without repairing.
|
|
263
|
+
drift[drift.length - 1] = {
|
|
264
|
+
item: 'legacy-markers-hand-edited',
|
|
265
|
+
file: name,
|
|
266
|
+
message: `${name} contains a legacy managed block with hand edits that do not match the pre-0.6.0 canonical fragment. Re-run with --force to overwrite (edits inside markers will be lost), or copy the hand-edited lines OUTSIDE the markers first.`,
|
|
267
|
+
refusedByFix: true,
|
|
268
|
+
};
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const applied = applyManagedBlockFix(text, ctx.canonical, blockOpts, ctx.canonicalHash);
|
|
273
|
+
if (applied && applied.action !== 'noop') {
|
|
274
|
+
await writeFile(path, applied.nextText, 'utf8');
|
|
275
|
+
writes.push({ file: name, action: applied.action });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return { drift, writes };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function messageForState(state, path) {
|
|
283
|
+
switch (state) {
|
|
284
|
+
case 'missing-block':
|
|
285
|
+
return `${path} has no managed block. \`rcf doctor --fix\` appends one at end of file.`;
|
|
286
|
+
case 'stale-hash':
|
|
287
|
+
return `${path} has a managed block whose text does not match the canonical hash. \`rcf doctor --fix\` rewrites it in place.`;
|
|
288
|
+
case 'legacy-markers':
|
|
289
|
+
return `${path} carries pre-0.6.0 markers. \`rcf doctor --fix\` migrates the block to the new markers and canonical text.`;
|
|
290
|
+
case 'orphan-marker':
|
|
291
|
+
return `orphan managed-block marker in ${path}. Repair by hand: pair the marker or remove the corrupted region, then re-run \`rcf doctor --fix\`.`;
|
|
292
|
+
case 'duplicate-block':
|
|
293
|
+
return `${path} contains more than one managed-block pair. Repair by hand: remove the stale pair, then re-run \`rcf doctor --fix\`.`;
|
|
294
|
+
default:
|
|
295
|
+
return `${path}: ${state}.`;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Detect whether a legacy-markers file's inner content diverges from
|
|
301
|
+
* the pre-0.6.0 canonical fragment as shipped in 0.4.x/0.5.x
|
|
302
|
+
* (§7.3 fail-safe). Hashes the extracted legacy inner content (trimmed,
|
|
303
|
+
* matching `writeAgentInstructions`'s pre-0.6.0 write convention: the
|
|
304
|
+
* fragment landed between markers as `\n${fragment}\n`, and the shipped
|
|
305
|
+
* `fragment` was itself `.trim()`-normalised by `loadHarnessFragment`,
|
|
306
|
+
* so the trimmed inner content is exactly the fragment) and checks
|
|
307
|
+
* membership in the shipped whitelist.
|
|
308
|
+
*
|
|
309
|
+
* Fail-safe by default: any hash NOT in the whitelist is treated as
|
|
310
|
+
* hand-edited. Empty whitelist (edge case) means "every legacy block
|
|
311
|
+
* is potentially hand-edited", which is the safer stance. The
|
|
312
|
+
* heuristic-based v0.6.0 draft (`length < 500`, `startsWith('## RCF')`
|
|
313
|
+
* denylist) was fail-open by design — a hand-edit that kept the
|
|
314
|
+
* leading `## RCF`, stayed above the length floor, and did not add one
|
|
315
|
+
* of the three named headers slipped past silently. This replacement
|
|
316
|
+
* inverts the safety profile so unknown content requires explicit
|
|
317
|
+
* `--force` in non-interactive mode.
|
|
318
|
+
*
|
|
319
|
+
* @param {string} fileText
|
|
320
|
+
* @param {Set<string>} legacyFragmentHashes - whitelist loaded via loadLegacyFragmentHashes.
|
|
321
|
+
* @returns {boolean}
|
|
322
|
+
*/
|
|
323
|
+
function detectLegacyHandEdits(fileText, legacyFragmentHashes) {
|
|
324
|
+
const inner = extractLegacyInner(fileText);
|
|
325
|
+
if (inner === null) return false;
|
|
326
|
+
const innerHash = hashInnerContent(inner);
|
|
327
|
+
return !legacyFragmentHashes.has(innerHash);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function extractLegacyInner(fileText) {
|
|
331
|
+
const beginAt = fileText.indexOf(LEGACY_MARKER_BEGIN);
|
|
332
|
+
if (beginAt < 0) return null;
|
|
333
|
+
const innerStart = beginAt + LEGACY_MARKER_BEGIN.length;
|
|
334
|
+
const endAt = fileText.indexOf(LEGACY_MARKER_END, innerStart);
|
|
335
|
+
if (endAt < 0) return null;
|
|
336
|
+
return fileText.slice(innerStart, endAt);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/* ------------------------------------------------------------------ */
|
|
340
|
+
/* Check: gitignore */
|
|
341
|
+
/* ------------------------------------------------------------------ */
|
|
342
|
+
|
|
343
|
+
async function runGitignoreCheck(ctx) {
|
|
344
|
+
const drift = [];
|
|
345
|
+
const writes = [];
|
|
346
|
+
const path = join(ctx.projectRoot, '.gitignore');
|
|
347
|
+
let text;
|
|
348
|
+
try {
|
|
349
|
+
text = await readFile(path, 'utf8');
|
|
350
|
+
} catch (err) {
|
|
351
|
+
if (/** @type {NodeJS.ErrnoException} */ (err).code === 'ENOENT') {
|
|
352
|
+
drift.push({
|
|
353
|
+
item: 'missing-file',
|
|
354
|
+
file: '.gitignore',
|
|
355
|
+
message: 'no .gitignore in project root. `rcf doctor --fix` creates it with only the managed block.',
|
|
356
|
+
refusedByFix: false,
|
|
357
|
+
});
|
|
358
|
+
if (ctx.fix) {
|
|
359
|
+
await writeFile(path, composeGitignoreBlock(), 'utf8');
|
|
360
|
+
writes.push({ file: '.gitignore', action: 'created' });
|
|
361
|
+
}
|
|
362
|
+
return { drift, writes };
|
|
363
|
+
}
|
|
364
|
+
throw err;
|
|
365
|
+
}
|
|
366
|
+
const beginCount = countOccurrencesLocal(text, GITIGNORE_MARKER_BEGIN);
|
|
367
|
+
const endCount = countOccurrencesLocal(text, GITIGNORE_MARKER_END);
|
|
368
|
+
if (beginCount >= 2 && endCount >= 2) {
|
|
369
|
+
drift.push({
|
|
370
|
+
item: 'duplicate-block',
|
|
371
|
+
file: '.gitignore',
|
|
372
|
+
message: '.gitignore contains more than one managed-block pair. Repair by hand: remove the stale pair, then re-run.',
|
|
373
|
+
refusedByFix: true,
|
|
374
|
+
});
|
|
375
|
+
return { drift, writes };
|
|
376
|
+
}
|
|
377
|
+
if (beginCount !== endCount) {
|
|
378
|
+
drift.push({
|
|
379
|
+
item: 'orphan-marker',
|
|
380
|
+
file: '.gitignore',
|
|
381
|
+
message: 'orphan managed-block marker in .gitignore. Repair by hand: pair the marker or remove the corrupted region.',
|
|
382
|
+
refusedByFix: true,
|
|
383
|
+
});
|
|
384
|
+
return { drift, writes };
|
|
385
|
+
}
|
|
386
|
+
if (beginCount === 0) {
|
|
387
|
+
drift.push({
|
|
388
|
+
item: 'missing-block',
|
|
389
|
+
file: '.gitignore',
|
|
390
|
+
message: '.gitignore has no managed block. `rcf doctor --fix` appends one at end of file.',
|
|
391
|
+
refusedByFix: false,
|
|
392
|
+
});
|
|
393
|
+
if (ctx.fix) {
|
|
394
|
+
const composed = composeGitignoreBlock();
|
|
395
|
+
const sep = text.length === 0 ? '' : (text.endsWith('\n') ? '\n' : '\n\n');
|
|
396
|
+
const next = `${text}${sep}${composed}`;
|
|
397
|
+
await writeFile(path, next, 'utf8');
|
|
398
|
+
writes.push({ file: '.gitignore', action: 'appended' });
|
|
399
|
+
}
|
|
400
|
+
return { drift, writes };
|
|
401
|
+
}
|
|
402
|
+
const located = extractGitignoreBlock(text);
|
|
403
|
+
if (!located) {
|
|
404
|
+
// Shouldn't happen given the counts above, but guard defensively.
|
|
405
|
+
drift.push({
|
|
406
|
+
item: 'orphan-marker',
|
|
407
|
+
file: '.gitignore',
|
|
408
|
+
message: 'orphan managed-block marker in .gitignore.',
|
|
409
|
+
refusedByFix: true,
|
|
410
|
+
});
|
|
411
|
+
return { drift, writes };
|
|
412
|
+
}
|
|
413
|
+
const innerHash = hashInnerContent(located.innerText);
|
|
414
|
+
const expected = computeGitignoreBlockHash();
|
|
415
|
+
if (innerHash === expected) {
|
|
416
|
+
return { drift, writes };
|
|
417
|
+
}
|
|
418
|
+
drift.push({
|
|
419
|
+
item: 'stale-hash',
|
|
420
|
+
file: '.gitignore',
|
|
421
|
+
message: '.gitignore managed block does not match the current aggregator output. `rcf doctor --fix` rewrites it.',
|
|
422
|
+
refusedByFix: false,
|
|
423
|
+
});
|
|
424
|
+
if (ctx.fix) {
|
|
425
|
+
const composed = composeGitignoreBlock();
|
|
426
|
+
const next = text.slice(0, located.beginIndex) + composed + text.slice(located.endIndex);
|
|
427
|
+
await writeFile(path, next, 'utf8');
|
|
428
|
+
writes.push({ file: '.gitignore', action: 'replaced' });
|
|
429
|
+
}
|
|
430
|
+
return { drift, writes };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function countOccurrencesLocal(haystack, needle) {
|
|
434
|
+
if (needle.length === 0) return 0;
|
|
435
|
+
let count = 0;
|
|
436
|
+
let i = 0;
|
|
437
|
+
while (true) {
|
|
438
|
+
const at = haystack.indexOf(needle, i);
|
|
439
|
+
if (at < 0) return count;
|
|
440
|
+
count += 1;
|
|
441
|
+
i = at + needle.length;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/* ------------------------------------------------------------------ */
|
|
446
|
+
/* Check: knowledge */
|
|
447
|
+
/* ------------------------------------------------------------------ */
|
|
448
|
+
|
|
449
|
+
async function runKnowledgeCheck(ctx) {
|
|
450
|
+
const drift = [];
|
|
451
|
+
const paths = knowledgePaths(ctx.projectRoot);
|
|
452
|
+
const rootExists = await pathExists(paths.dir);
|
|
453
|
+
if (!rootExists) {
|
|
454
|
+
drift.push({
|
|
455
|
+
item: 'missing-directory',
|
|
456
|
+
file: 'rcf/knowledge/',
|
|
457
|
+
message: 'rcf/knowledge/ is missing (0.6.0 convention). Run `rcf init` to seed it, or create the directory by hand.',
|
|
458
|
+
refusedByFix: true,
|
|
459
|
+
});
|
|
460
|
+
return { drift, writes: [] };
|
|
461
|
+
}
|
|
462
|
+
const notesExists = await pathExists(join(paths.dir, 'notes'));
|
|
463
|
+
const docsExists = await pathExists(join(paths.dir, 'docs'));
|
|
464
|
+
if (!notesExists) {
|
|
465
|
+
drift.push({
|
|
466
|
+
item: 'missing-subdir',
|
|
467
|
+
file: 'rcf/knowledge/notes/',
|
|
468
|
+
message: 'rcf/knowledge/notes/ is missing. Run `rcf init` to re-seed, or create the directory by hand.',
|
|
469
|
+
refusedByFix: true,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
if (!docsExists) {
|
|
473
|
+
drift.push({
|
|
474
|
+
item: 'missing-subdir',
|
|
475
|
+
file: 'rcf/knowledge/docs/',
|
|
476
|
+
message: 'rcf/knowledge/docs/ is missing. Run `rcf init` to re-seed, or create the directory by hand.',
|
|
477
|
+
refusedByFix: true,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
return { drift, writes: [] };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function pathExists(path) {
|
|
484
|
+
try {
|
|
485
|
+
await stat(path);
|
|
486
|
+
return true;
|
|
487
|
+
} catch (err) {
|
|
488
|
+
if (/** @type {NodeJS.ErrnoException} */ (err).code === 'ENOENT') return false;
|
|
489
|
+
throw err;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/* ------------------------------------------------------------------ */
|
|
494
|
+
/* Check: identity */
|
|
495
|
+
/* ------------------------------------------------------------------ */
|
|
496
|
+
|
|
497
|
+
async function runIdentityCheck(ctx) {
|
|
498
|
+
const drift = [];
|
|
499
|
+
const path = identityProfilePath(ctx.projectRoot);
|
|
500
|
+
const profileExists = await pathExists(path);
|
|
501
|
+
if (!profileExists) {
|
|
502
|
+
drift.push({
|
|
503
|
+
item: 'missing-file',
|
|
504
|
+
file: 'rcf/.identity/profile.md',
|
|
505
|
+
message: 'optional; profile.md is the recommended template but not required. Run `rcf init` to seed it, or create it by hand.',
|
|
506
|
+
refusedByFix: true,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
const ignored = await isPathIgnored(ctx.projectRoot);
|
|
510
|
+
if (profileExists && !ignored) {
|
|
511
|
+
drift.push({
|
|
512
|
+
item: 'gitignore-mismatch',
|
|
513
|
+
file: 'rcf/.identity/',
|
|
514
|
+
message: '`rcf/.identity/` is not gitignored; the profile may be exposed. Check your `.gitignore`.',
|
|
515
|
+
refusedByFix: true,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
return { drift, writes: [] };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Coarse effective-ignore check: does the project's .gitignore contain
|
|
523
|
+
* a line that matches `rcf/.identity/`? We look inside the managed
|
|
524
|
+
* block first (the expected home) and then scan any operator-owned
|
|
525
|
+
* lines. Real git semantics are more subtle (later-match wins,
|
|
526
|
+
* unignore patterns, .git/info/exclude); doctor's job here is to warn
|
|
527
|
+
* on the common case, and the AC-4.3 test uses `git check-ignore` for
|
|
528
|
+
* the definitive assertion. If any registered aggregator entry with
|
|
529
|
+
* path `rcf/.identity/` appears anywhere in the .gitignore text and
|
|
530
|
+
* there is no bare `!rcf/.identity/` unignore, we treat it as ignored.
|
|
531
|
+
*/
|
|
532
|
+
async function isPathIgnored(projectRoot) {
|
|
533
|
+
try {
|
|
534
|
+
const text = await readFile(join(projectRoot, '.gitignore'), 'utf8');
|
|
535
|
+
const wantsIgnored = managedGitignoreEntries()
|
|
536
|
+
.some((e) => e.path === 'rcf/.identity/')
|
|
537
|
+
|| false;
|
|
538
|
+
const lines = text.split('\n').map((l) => l.trim());
|
|
539
|
+
const ignored = lines.includes('rcf/.identity/');
|
|
540
|
+
const unignored = lines.includes('!rcf/.identity/') || lines.includes('!rcf/.identity/*');
|
|
541
|
+
return wantsIgnored && ignored && !unignored;
|
|
542
|
+
} catch {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Silence unused-import warnings for values consumed only via names.
|
|
548
|
+
void composeGitignoreInner;
|