@interf/compiler 0.22.2 → 0.50.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/README.md +119 -282
- package/dist/bin-mcp.d.ts +2 -0
- package/dist/bin-mcp.js +63 -0
- package/dist/bin-runtime.d.ts +2 -0
- package/dist/bin-runtime.js +111 -0
- package/dist/cli/commands/agents.js +4 -35
- package/dist/cli/commands/auth.d.ts +20 -0
- package/dist/cli/commands/auth.js +161 -0
- package/dist/cli/commands/benchmark.d.ts +9 -0
- package/dist/cli/commands/benchmark.js +58 -0
- package/dist/cli/commands/build-plan.js +107 -139
- package/dist/cli/commands/build.d.ts +3 -4
- package/dist/cli/commands/build.js +16 -45
- package/dist/cli/commands/doctor.js +3 -3
- package/dist/cli/commands/graphs.d.ts +2 -0
- package/dist/cli/commands/graphs.js +344 -0
- package/dist/cli/commands/login.js +4 -6
- package/dist/cli/commands/logout.js +1 -1
- package/dist/cli/commands/mcp.d.ts +4 -2
- package/dist/cli/commands/mcp.js +846 -232
- package/dist/cli/commands/project.d.ts +2 -0
- package/dist/cli/commands/project.js +176 -0
- package/dist/cli/commands/reset.d.ts +3 -4
- package/dist/cli/commands/reset.js +10 -31
- package/dist/cli/commands/runs.js +136 -57
- package/dist/cli/commands/runtime.d.ts +24 -0
- package/dist/cli/commands/runtime.js +373 -0
- package/dist/cli/commands/status.d.ts +1 -0
- package/dist/cli/commands/status.js +35 -45
- package/dist/cli/commands/traces.d.ts +2 -0
- package/dist/cli/commands/traces.js +97 -0
- package/dist/cli/commands/wizard.js +171 -178
- package/dist/cli/index.d.ts +7 -4
- package/dist/cli/index.js +13 -7
- package/dist/cli/lib/http-client.d.ts +39 -0
- package/dist/cli/lib/http-client.js +73 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/packages/build-plans/authoring/brief.d.ts +538 -0
- package/dist/packages/build-plans/authoring/brief.js +89 -0
- package/dist/packages/build-plans/authoring/build-plan-authoring.d.ts +52 -11
- package/dist/packages/build-plans/authoring/build-plan-authoring.js +493 -46
- package/dist/packages/build-plans/authoring/build-plan-edit-session.d.ts +10 -1
- package/dist/packages/build-plans/authoring/build-plan-edit-session.js +27 -4
- package/dist/packages/build-plans/authoring/build-plan-improvement.d.ts +9 -6
- package/dist/packages/build-plans/authoring/build-plan-improvement.js +97 -46
- package/dist/packages/build-plans/authoring/lib/build-plan-edit-utils.d.ts +1 -0
- package/dist/packages/build-plans/authoring/lib/build-plan-edit-utils.js +7 -7
- package/dist/packages/build-plans/build-plan-resolution.d.ts +1 -1
- package/dist/packages/build-plans/build-plan-resolution.js +3 -3
- package/dist/packages/build-plans/index.d.ts +1 -1
- package/dist/packages/build-plans/index.js +1 -1
- package/dist/packages/build-plans/package/build-plan-definitions.d.ts +14 -13
- package/dist/packages/build-plans/package/build-plan-definitions.js +45 -42
- package/dist/packages/build-plans/package/build-plan-helpers.d.ts +3 -2
- package/dist/packages/build-plans/package/build-plan-helpers.js +27 -13
- package/dist/packages/build-plans/package/build-plan-review-paths.d.ts +5 -5
- package/dist/packages/build-plans/package/build-plan-review-paths.js +15 -15
- package/dist/packages/build-plans/package/build-plan-stage-runner.d.ts +5 -4
- package/dist/packages/build-plans/package/build-plan-stage-runner.js +23 -11
- package/dist/packages/build-plans/package/builtin-build-plan.d.ts +7 -8
- package/dist/packages/build-plans/package/builtin-build-plan.js +10 -11
- package/dist/packages/build-plans/package/context-interface.d.ts +14 -9
- package/dist/packages/build-plans/package/context-interface.js +14 -33
- package/dist/packages/build-plans/package/interf-build-plan-package.d.ts +6 -17
- package/dist/packages/build-plans/package/interf-build-plan-package.js +68 -64
- package/dist/packages/build-plans/package/local-build-plans.d.ts +21 -14
- package/dist/packages/build-plans/package/local-build-plans.js +105 -55
- package/dist/packages/build-plans/package/user-build-plans.js +1 -1
- package/dist/packages/contracts/index.d.ts +5 -2
- package/dist/packages/contracts/index.js +3 -1
- package/dist/packages/contracts/lib/context-graph-layer.d.ts +161 -0
- package/dist/packages/contracts/lib/context-graph-layer.js +216 -0
- package/dist/packages/contracts/lib/project-paths.d.ts +144 -0
- package/dist/packages/contracts/lib/project-paths.js +220 -0
- package/dist/packages/contracts/lib/project-schema.d.ts +423 -0
- package/dist/packages/contracts/lib/project-schema.js +138 -0
- package/dist/packages/contracts/lib/schema.d.ts +1273 -81
- package/dist/packages/contracts/lib/schema.js +675 -79
- package/dist/packages/contracts/utils/filesystem.d.ts +1 -0
- package/dist/packages/contracts/utils/filesystem.js +29 -1
- package/dist/packages/contracts/utils/parse.js +67 -0
- package/dist/packages/projects/index.d.ts +6 -0
- package/dist/packages/{project → projects}/index.js +0 -3
- package/dist/packages/{project → projects}/interf-detect.d.ts +12 -12
- package/dist/packages/{project → projects}/interf-detect.js +56 -50
- package/dist/packages/projects/interf.d.ts +2 -0
- package/dist/packages/projects/interf.js +1 -0
- package/dist/packages/projects/lib/schema.d.ts +77 -0
- package/dist/packages/projects/lib/schema.js +91 -0
- package/dist/packages/projects/source-config.d.ts +53 -0
- package/dist/packages/projects/source-config.js +339 -0
- package/dist/packages/projects/source-folders.d.ts +11 -0
- package/dist/packages/{project → projects}/source-folders.js +26 -26
- package/dist/packages/{engine → runtime}/action-planner.d.ts +1 -1
- package/dist/packages/{engine → runtime}/action-planner.js +20 -22
- package/dist/packages/runtime/action-values.d.ts +1 -0
- package/dist/packages/runtime/action-values.js +1 -0
- package/dist/packages/runtime/actions/errors.d.ts +2 -0
- package/dist/packages/runtime/actions/errors.js +12 -0
- package/dist/packages/runtime/actions/fields.d.ts +86 -0
- package/dist/packages/runtime/actions/form-builders.d.ts +14 -0
- package/dist/packages/runtime/actions/form-builders.js +667 -0
- package/dist/packages/runtime/actions/form-validators.d.ts +8 -0
- package/dist/packages/runtime/actions/form-validators.js +134 -0
- package/dist/packages/runtime/actions/helpers.d.ts +11 -0
- package/dist/packages/runtime/actions/helpers.js +80 -0
- package/dist/packages/runtime/actions/index.d.ts +8 -0
- package/dist/packages/runtime/actions/index.js +11 -0
- package/dist/packages/runtime/actions/registry.d.ts +64 -0
- package/dist/packages/runtime/actions/registry.js +62 -0
- package/dist/packages/runtime/actions/requests.d.ts +45 -0
- package/dist/packages/runtime/actions/requests.js +164 -0
- package/dist/packages/runtime/actions/schemas.d.ts +161 -0
- package/dist/packages/runtime/actions/schemas.js +37 -0
- package/dist/packages/runtime/agent-handoff.d.ts +11 -0
- package/dist/packages/runtime/agent-handoff.js +102 -0
- package/dist/packages/{engine → runtime}/agents/index.d.ts +1 -2
- package/dist/packages/{engine → runtime}/agents/index.js +1 -2
- package/dist/packages/runtime/agents/lib/args.d.ts +14 -0
- package/dist/packages/runtime/agents/lib/args.js +24 -0
- package/dist/packages/{engine → runtime}/agents/lib/constants.d.ts +4 -1
- package/dist/packages/runtime/agents/lib/constants.js +13 -0
- package/dist/packages/runtime/agents/lib/context-graph-bootstrap.d.ts +3 -0
- package/dist/packages/{engine/agents/lib/verifiable-context-bootstrap.js → runtime/agents/lib/context-graph-bootstrap.js} +5 -6
- package/dist/packages/{engine → runtime}/agents/lib/detection.d.ts +5 -0
- package/dist/packages/{engine → runtime}/agents/lib/detection.js +16 -7
- package/dist/packages/{engine → runtime}/agents/lib/execution-profile.d.ts +14 -0
- package/dist/packages/{engine → runtime}/agents/lib/execution-profile.js +31 -14
- package/dist/packages/{engine → runtime}/agents/lib/execution.js +22 -6
- package/dist/packages/{engine → runtime}/agents/lib/executors.d.ts +1 -0
- package/dist/packages/{engine → runtime}/agents/lib/executors.js +11 -2
- package/dist/packages/runtime/agents/lib/logs.d.ts +12 -0
- package/dist/packages/runtime/agents/lib/logs.js +41 -0
- package/dist/packages/{engine → runtime}/agents/lib/preflight.js +19 -14
- package/dist/packages/runtime/agents/lib/render.d.ts +26 -0
- package/dist/packages/{engine → runtime}/agents/lib/render.js +48 -22
- package/dist/packages/runtime/agents/lib/shell-fs.d.ts +18 -0
- package/dist/packages/runtime/agents/lib/shell-fs.js +190 -0
- package/dist/packages/runtime/agents/lib/shell-paths.d.ts +16 -0
- package/dist/packages/runtime/agents/lib/shell-paths.js +63 -0
- package/dist/packages/runtime/agents/lib/shell-projection.d.ts +25 -0
- package/dist/packages/runtime/agents/lib/shell-projection.js +314 -0
- package/dist/packages/runtime/agents/lib/shell-templates.d.ts +30 -0
- package/dist/packages/runtime/agents/lib/shell-templates.js +494 -0
- package/dist/packages/runtime/agents/lib/shell-workspace.d.ts +17 -0
- package/dist/packages/runtime/agents/lib/shell-workspace.js +70 -0
- package/dist/packages/runtime/agents/lib/shells.d.ts +92 -0
- package/dist/packages/runtime/agents/lib/shells.js +509 -0
- package/dist/packages/runtime/agents/lib/source-context-scan.d.ts +10 -0
- package/dist/packages/runtime/agents/lib/source-context-scan.js +388 -0
- package/dist/packages/{engine → runtime}/agents/lib/status.js +1 -14
- package/dist/packages/runtime/agents/lib/string-utils.d.ts +16 -0
- package/dist/packages/runtime/agents/lib/string-utils.js +36 -0
- package/dist/packages/{engine → runtime}/agents/lib/types.d.ts +1 -0
- package/dist/packages/{engine → runtime}/agents/lib/user-config.d.ts +8 -2
- package/dist/packages/{engine → runtime}/agents/lib/user-config.js +8 -2
- package/dist/packages/runtime/agents/providers/claude-code.d.ts +13 -0
- package/dist/packages/runtime/agents/providers/claude-code.js +45 -0
- package/dist/packages/runtime/agents/providers/codex.d.ts +17 -0
- package/dist/packages/runtime/agents/providers/codex.js +66 -0
- package/dist/packages/runtime/agents/providers/cursor.d.ts +9 -0
- package/dist/packages/runtime/agents/providers/cursor.js +24 -0
- package/dist/packages/runtime/agents/providers/index.d.ts +9 -0
- package/dist/packages/runtime/agents/providers/index.js +31 -0
- package/dist/packages/runtime/agents/providers/types.d.ts +50 -0
- package/dist/packages/{engine → runtime}/agents/registry.d.ts +13 -2
- package/dist/packages/{engine → runtime}/agents/registry.js +48 -10
- package/dist/packages/{engine → runtime}/agents/role-executors.d.ts +1 -1
- package/dist/packages/{engine → runtime}/agents/role-executors.js +9 -7
- package/dist/packages/{engine → runtime}/agents/role-router.js +7 -5
- package/dist/packages/runtime/auth/account-context.d.ts +52 -0
- package/dist/packages/runtime/auth/account-context.js +68 -0
- package/dist/packages/runtime/auth/auth-flow.d.ts +73 -0
- package/dist/packages/runtime/auth/auth-flow.js +189 -0
- package/dist/packages/runtime/auth/jwt-validator.d.ts +58 -0
- package/dist/packages/runtime/auth/jwt-validator.js +86 -0
- package/dist/packages/runtime/auth/keychain.d.ts +35 -0
- package/dist/packages/runtime/auth/keychain.js +85 -0
- package/dist/packages/runtime/auth/session-store.d.ts +38 -0
- package/dist/packages/runtime/auth/session-store.js +96 -0
- package/dist/packages/runtime/auth/workos-client.d.ts +58 -0
- package/dist/packages/runtime/auth/workos-client.js +87 -0
- package/dist/packages/runtime/benchmark-question-draft.d.ts +23 -0
- package/dist/packages/runtime/benchmark-question-draft.js +153 -0
- package/dist/packages/runtime/build/artifact-counts.d.ts +1 -0
- package/dist/packages/{engine → runtime}/build/artifact-counts.js +5 -9
- package/dist/packages/{engine → runtime}/build/artifact-status.d.ts +6 -6
- package/dist/packages/{engine → runtime}/build/artifact-status.js +26 -24
- package/dist/packages/runtime/build/atomic-fs.d.ts +3 -0
- package/dist/packages/runtime/build/atomic-fs.js +95 -0
- package/dist/packages/runtime/build/billing-events.d.ts +78 -0
- package/dist/packages/{engine → runtime}/build/billing-events.js +17 -19
- package/dist/packages/runtime/build/build-evidence.d.ts +16 -0
- package/dist/packages/runtime/build/build-evidence.js +179 -0
- package/dist/packages/{engine → runtime}/build/build-pipeline.d.ts +12 -8
- package/dist/packages/runtime/build/build-pipeline.js +388 -0
- package/dist/packages/{engine → runtime}/build/build-plan-primitives.d.ts +1 -1
- package/dist/packages/{engine → runtime}/build/build-plan-primitives.js +0 -1
- package/dist/packages/runtime/build/build-plan-runs.d.ts +14 -0
- package/dist/packages/runtime/build/build-plan-runs.js +31 -0
- package/dist/packages/runtime/build/build-stage-plan.d.ts +16 -0
- package/dist/packages/runtime/build/build-stage-plan.js +101 -0
- package/dist/packages/{engine → runtime}/build/build-stage-runner.d.ts +2 -1
- package/dist/packages/runtime/build/build-stage-runner.js +302 -0
- package/dist/packages/{engine → runtime}/build/build-target.d.ts +7 -4
- package/dist/packages/runtime/build/build-target.js +40 -0
- package/dist/packages/{engine → runtime}/build/check-evaluator.d.ts +14 -16
- package/dist/packages/runtime/build/check-evaluator.js +1226 -0
- package/dist/packages/runtime/build/context-graph-paths.d.ts +64 -0
- package/dist/packages/runtime/build/context-graph-paths.js +160 -0
- package/dist/packages/runtime/build/context-graph-schema.d.ts +19 -0
- package/dist/packages/runtime/build/context-graph-schema.js +39 -0
- package/dist/packages/{engine → runtime}/build/discovery.d.ts +2 -2
- package/dist/packages/{engine → runtime}/build/discovery.js +4 -4
- package/dist/packages/{engine → runtime}/build/index.d.ts +7 -5
- package/dist/packages/{engine → runtime}/build/index.js +7 -5
- package/dist/packages/runtime/build/inspect-map.d.ts +10 -0
- package/dist/packages/runtime/build/inspect-map.js +270 -0
- package/dist/packages/{engine → runtime}/build/lib/schema.d.ts +449 -123
- package/dist/packages/runtime/build/lib/schema.js +494 -0
- package/dist/packages/runtime/build/native-entrypoint.d.ts +2 -0
- package/dist/packages/runtime/build/native-entrypoint.js +286 -0
- package/dist/packages/runtime/build/reset.d.ts +2 -0
- package/dist/packages/runtime/build/reset.js +62 -0
- package/dist/packages/{engine → runtime}/build/runtime-contracts.js +13 -7
- package/dist/packages/runtime/build/runtime-inventory.d.ts +7 -0
- package/dist/packages/{engine → runtime}/build/runtime-inventory.js +3 -3
- package/dist/packages/runtime/build/runtime-log-paths.d.ts +3 -0
- package/dist/packages/runtime/build/runtime-log-paths.js +16 -0
- package/dist/packages/{engine → runtime}/build/runtime-prompt.js +12 -9
- package/dist/packages/{engine → runtime}/build/runtime-reconcile.d.ts +1 -1
- package/dist/packages/{engine → runtime}/build/runtime-reconcile.js +25 -21
- package/dist/packages/runtime/build/runtime-runs.d.ts +10 -0
- package/dist/packages/runtime/build/runtime-runs.js +318 -0
- package/dist/packages/{engine → runtime}/build/runtime-types.d.ts +9 -6
- package/dist/packages/runtime/build/runtime-types.js +1 -0
- package/dist/packages/runtime/build/runtime.d.ts +8 -0
- package/dist/packages/runtime/build/runtime.js +7 -0
- package/dist/packages/runtime/build/source-files.d.ts +58 -0
- package/dist/packages/runtime/build/source-files.js +193 -0
- package/dist/packages/runtime/build/source-inventory.d.ts +28 -0
- package/dist/packages/runtime/build/source-inventory.js +512 -0
- package/dist/packages/runtime/build/source-manifest.d.ts +63 -0
- package/dist/packages/runtime/build/source-manifest.js +220 -0
- package/dist/packages/runtime/build/stage-evidence.d.ts +22 -0
- package/dist/packages/runtime/build/stage-evidence.js +386 -0
- package/dist/packages/runtime/build/stage-manifest.d.ts +45 -0
- package/dist/packages/runtime/build/stage-manifest.js +1125 -0
- package/dist/packages/runtime/build/stage-reuse.d.ts +11 -0
- package/dist/packages/runtime/build/stage-reuse.js +154 -0
- package/dist/packages/runtime/build/stage-session.d.ts +81 -0
- package/dist/packages/runtime/build/stage-session.js +308 -0
- package/dist/packages/runtime/build/state-artifacts.d.ts +9 -0
- package/dist/packages/runtime/build/state-artifacts.js +14 -0
- package/dist/packages/runtime/build/state-health.d.ts +4 -0
- package/dist/packages/{engine → runtime}/build/state-health.js +21 -26
- package/dist/packages/runtime/build/state-io.d.ts +12 -0
- package/dist/packages/runtime/build/state-io.js +118 -0
- package/dist/packages/runtime/build/state-view.d.ts +5 -0
- package/dist/packages/runtime/build/state-view.js +121 -0
- package/dist/packages/runtime/build/state.d.ts +7 -0
- package/dist/packages/runtime/build/state.js +12 -0
- package/dist/packages/runtime/build/summary-coverage-index.d.ts +21 -0
- package/dist/packages/runtime/build/summary-coverage-index.js +189 -0
- package/dist/packages/runtime/build/traces.d.ts +30 -0
- package/dist/packages/runtime/build/traces.js +133 -0
- package/dist/packages/{engine/build/validate-verifiable-context.d.ts → runtime/build/validate-context-graph.d.ts} +6 -6
- package/dist/packages/{engine/build/validate-verifiable-context.js → runtime/build/validate-context-graph.js} +49 -36
- package/dist/packages/{engine → runtime}/build/validate.d.ts +5 -5
- package/dist/packages/{engine → runtime}/build/validate.js +26 -26
- package/dist/packages/{engine → runtime}/client.d.ts +18 -18
- package/dist/packages/{engine → runtime}/client.js +48 -36
- package/dist/packages/{engine → runtime}/connection-config.d.ts +3 -2
- package/dist/packages/{engine → runtime}/connection-config.js +9 -8
- package/dist/packages/runtime/context-checks.d.ts +10 -0
- package/dist/packages/runtime/context-checks.js +127 -0
- package/dist/packages/runtime/context-graph-scaffold.d.ts +9 -0
- package/dist/packages/runtime/context-graph-scaffold.js +135 -0
- package/dist/packages/runtime/context-graph-semantic-graph.d.ts +9 -0
- package/dist/packages/runtime/context-graph-semantic-graph.js +416 -0
- package/dist/packages/runtime/entitlement-guard.d.ts +43 -0
- package/dist/packages/runtime/entitlement-guard.js +70 -0
- package/dist/packages/{engine → runtime}/execution/index.d.ts +2 -2
- package/dist/packages/{engine → runtime}/execution/index.js +1 -1
- package/dist/packages/{engine → runtime}/execution/lib/schema.d.ts +272 -191
- package/dist/packages/{engine → runtime}/execution/lib/schema.js +35 -32
- package/dist/packages/runtime/index.d.ts +29 -0
- package/dist/packages/runtime/index.js +21 -0
- package/dist/packages/runtime/instance-paths.d.ts +30 -0
- package/dist/packages/runtime/instance-paths.js +29 -0
- package/dist/packages/runtime/native-run-handlers.d.ts +63 -0
- package/dist/packages/{engine → runtime}/native-run-handlers.js +217 -166
- package/dist/packages/runtime/plan-artifact-contract.d.ts +17 -0
- package/dist/packages/runtime/plan-artifact-contract.js +42 -0
- package/dist/packages/runtime/project-entries.d.ts +11 -0
- package/dist/packages/runtime/project-entries.js +49 -0
- package/dist/packages/runtime/project-source-state.d.ts +26 -0
- package/dist/packages/runtime/project-source-state.js +56 -0
- package/dist/packages/runtime/project-store.d.ts +90 -0
- package/dist/packages/runtime/project-store.js +195 -0
- package/dist/packages/runtime/requested-artifacts.d.ts +7 -0
- package/dist/packages/{engine → runtime}/requested-artifacts.js +23 -1
- package/dist/packages/{engine → runtime}/run-observability.d.ts +2 -1
- package/dist/packages/{engine → runtime}/run-observability.js +174 -87
- package/dist/packages/runtime/runtime-action-proposals.d.ts +7 -0
- package/dist/packages/runtime/runtime-action-proposals.js +542 -0
- package/dist/packages/runtime/runtime-build-plans.d.ts +5 -0
- package/dist/packages/runtime/runtime-build-plans.js +175 -0
- package/dist/packages/runtime/runtime-build-runs.d.ts +47 -0
- package/dist/packages/runtime/runtime-build-runs.js +555 -0
- package/dist/packages/runtime/runtime-caches.d.ts +117 -0
- package/dist/packages/runtime/runtime-caches.js +266 -0
- package/dist/packages/{engine → runtime}/runtime-event-applier.d.ts +3 -1
- package/dist/packages/{engine → runtime}/runtime-event-applier.js +53 -17
- package/dist/packages/runtime/runtime-executor.d.ts +22 -0
- package/dist/packages/runtime/runtime-executor.js +131 -0
- package/dist/packages/runtime/runtime-jobs.d.ts +13 -0
- package/dist/packages/runtime/runtime-jobs.js +463 -0
- package/dist/packages/runtime/runtime-observability.d.ts +11 -0
- package/dist/packages/runtime/runtime-observability.js +39 -0
- package/dist/packages/{engine → runtime}/runtime-persistence.d.ts +9 -18
- package/dist/packages/{engine → runtime}/runtime-persistence.js +25 -25
- package/dist/packages/runtime/runtime-project-mutations.d.ts +7 -0
- package/dist/packages/runtime/runtime-project-mutations.js +65 -0
- package/dist/packages/runtime/runtime-project-reads.d.ts +18 -0
- package/dist/packages/runtime/runtime-project-reads.js +574 -0
- package/dist/packages/runtime/runtime-proposal-helpers.d.ts +22 -0
- package/dist/packages/runtime/runtime-proposal-helpers.js +223 -0
- package/dist/packages/{engine → runtime}/runtime-resource-builders.d.ts +23 -16
- package/dist/packages/{engine → runtime}/runtime-resource-builders.js +58 -46
- package/dist/packages/runtime/runtime-status.d.ts +14 -0
- package/dist/packages/runtime/runtime-status.js +15 -0
- package/dist/packages/runtime/runtime-verify-runs.d.ts +84 -0
- package/dist/packages/runtime/runtime-verify-runs.js +296 -0
- package/dist/packages/runtime/runtime.d.ts +1582 -0
- package/dist/packages/runtime/runtime.js +431 -0
- package/dist/packages/runtime/schemas/actions.d.ts +1206 -0
- package/dist/packages/runtime/schemas/actions.js +117 -0
- package/dist/packages/runtime/schemas/agents.d.ts +104 -0
- package/dist/packages/runtime/schemas/agents.js +74 -0
- package/dist/packages/runtime/schemas/build-plans.d.ts +1132 -0
- package/dist/packages/runtime/schemas/build-plans.js +141 -0
- package/dist/packages/runtime/schemas/context-graphs.d.ts +1522 -0
- package/dist/packages/runtime/schemas/context-graphs.js +110 -0
- package/dist/packages/runtime/schemas/files.d.ts +227 -0
- package/dist/packages/runtime/schemas/files.js +28 -0
- package/dist/packages/runtime/schemas/index.d.ts +9 -0
- package/dist/packages/runtime/schemas/index.js +13 -0
- package/dist/packages/runtime/schemas/instance.d.ts +141 -0
- package/dist/packages/runtime/schemas/instance.js +143 -0
- package/dist/packages/runtime/schemas/jobs.d.ts +339 -0
- package/dist/packages/runtime/schemas/jobs.js +107 -0
- package/dist/packages/runtime/schemas/projects.d.ts +366 -0
- package/dist/packages/runtime/schemas/projects.js +160 -0
- package/dist/packages/runtime/schemas/runs.d.ts +3445 -0
- package/dist/packages/runtime/schemas/runs.js +115 -0
- package/dist/packages/runtime/service/index.d.ts +3 -0
- package/dist/packages/runtime/service/index.js +3 -0
- package/dist/packages/runtime/service/openapi.d.ts +7 -0
- package/dist/packages/runtime/service/openapi.js +118 -0
- package/dist/packages/runtime/service/operations.d.ts +3011 -0
- package/dist/packages/runtime/service/operations.js +375 -0
- package/dist/packages/runtime/service/routes.d.ts +114 -0
- package/dist/packages/runtime/service/routes.js +128 -0
- package/dist/packages/runtime/service/server-api-files.d.ts +10 -0
- package/dist/packages/runtime/service/server-api-files.js +85 -0
- package/dist/packages/runtime/service/server-app-boot.d.ts +4 -0
- package/dist/packages/runtime/service/server-app-boot.js +46 -0
- package/dist/packages/runtime/service/server-guards.d.ts +63 -0
- package/dist/packages/runtime/service/server-guards.js +181 -0
- package/dist/packages/runtime/service/server-helpers.d.ts +38 -0
- package/dist/packages/runtime/service/server-helpers.js +108 -0
- package/dist/packages/runtime/service/server-instance-helpers.d.ts +30 -0
- package/dist/packages/runtime/service/server-instance-helpers.js +114 -0
- package/dist/packages/runtime/service/server-routes-action-proposals.d.ts +3 -0
- package/dist/packages/runtime/service/server-routes-action-proposals.js +45 -0
- package/dist/packages/runtime/service/server-routes-agents.d.ts +4 -0
- package/dist/packages/runtime/service/server-routes-agents.js +132 -0
- package/dist/packages/runtime/service/server-routes-auth.d.ts +33 -0
- package/dist/packages/runtime/service/server-routes-auth.js +138 -0
- package/dist/packages/runtime/service/server-routes-build-plans.d.ts +3 -0
- package/dist/packages/runtime/service/server-routes-build-plans.js +86 -0
- package/dist/packages/runtime/service/server-routes-discovery.d.ts +4 -0
- package/dist/packages/runtime/service/server-routes-discovery.js +196 -0
- package/dist/packages/runtime/service/server-routes-events.d.ts +5 -0
- package/dist/packages/runtime/service/server-routes-events.js +99 -0
- package/dist/packages/runtime/service/server-routes-project-context.d.ts +9 -0
- package/dist/packages/runtime/service/server-routes-project-context.js +287 -0
- package/dist/packages/runtime/service/server-routes-project-jobs.d.ts +9 -0
- package/dist/packages/runtime/service/server-routes-project-jobs.js +137 -0
- package/dist/packages/runtime/service/server-routes-project-runs.d.ts +14 -0
- package/dist/packages/runtime/service/server-routes-project-runs.js +88 -0
- package/dist/packages/runtime/service/server-routes-projects.d.ts +4 -0
- package/dist/packages/runtime/service/server-routes-projects.js +96 -0
- package/dist/packages/runtime/service/server-routes-runs.d.ts +3 -0
- package/dist/packages/runtime/service/server-routes-runs.js +119 -0
- package/dist/packages/runtime/service/server.d.ts +37 -0
- package/dist/packages/runtime/service/server.js +300 -0
- package/dist/packages/{engine → runtime/service}/service-registry.d.ts +5 -5
- package/dist/packages/{engine → runtime/service}/service-registry.js +7 -7
- package/dist/packages/runtime/verify/benchmark-run.d.ts +81 -0
- package/dist/packages/runtime/verify/benchmark-run.js +303 -0
- package/dist/packages/{engine → runtime}/verify/index.d.ts +2 -2
- package/dist/packages/{engine → runtime}/verify/index.js +1 -1
- package/dist/packages/{engine → runtime}/verify/lib/schema.d.ts +83 -16
- package/dist/packages/{engine → runtime}/verify/lib/schema.js +38 -18
- package/dist/packages/runtime/verify/test-file-guard.d.ts +2 -0
- package/dist/packages/runtime/verify/test-file-guard.js +29 -0
- package/dist/packages/{engine → runtime}/verify/verify-execution.d.ts +7 -0
- package/dist/packages/{engine → runtime}/verify/verify-execution.js +119 -45
- package/dist/packages/{engine → runtime}/verify/verify-paths.d.ts +5 -4
- package/dist/packages/runtime/verify/verify-paths.js +65 -0
- package/dist/packages/{engine → runtime}/verify/verify-sandbox.d.ts +1 -1
- package/dist/packages/runtime/verify/verify-sandbox.js +88 -0
- package/dist/packages/{engine → runtime}/verify/verify-specs.d.ts +2 -0
- package/dist/packages/runtime/verify/verify-specs.js +126 -0
- package/dist/packages/runtime/verify/verify-targets.d.ts +5 -0
- package/dist/packages/{engine → runtime}/verify/verify-targets.js +12 -12
- package/dist/packages/runtime/verify/verify-types.js +1 -0
- package/dist/packages/{engine → runtime}/verify/verify.d.ts +1 -1
- package/dist/packages/{engine → runtime}/verify/verify.js +1 -1
- package/dist/packages/runtime/wire-schemas.d.ts +18 -0
- package/dist/packages/runtime/wire-schemas.js +27 -0
- package/package.json +32 -30
- package/public-repo/CONTRIBUTING.md +16 -18
- package/public-repo/README.md +119 -282
- package/public-repo/SECURITY.md +3 -4
- package/public-repo/build-plans/interf-default/README.md +24 -16
- package/public-repo/build-plans/interf-default/build/stages/entrypoint/SKILL.md +74 -0
- package/public-repo/build-plans/interf-default/build/stages/knowledge/SKILL.md +95 -0
- package/public-repo/build-plans/interf-default/build/stages/summarize/SKILL.md +49 -4
- package/public-repo/build-plans/interf-default/build-plan.json +49 -39
- package/public-repo/build-plans/interf-default/build-plan.schema.json +59 -33
- package/public-repo/build-plans/interf-default/improve/SKILL.md +3 -3
- package/public-repo/build-plans/interf-default/use/query/SKILL.md +18 -11
- package/public-repo/openapi/local-service.openapi.json +14227 -0
- package/public-repo/skills/interf/SKILL.md +508 -187
- package/dist/cli/commands/prep.d.ts +0 -2
- package/dist/cli/commands/prep.js +0 -240
- package/dist/cli/commands/test.d.ts +0 -10
- package/dist/cli/commands/test.js +0 -85
- package/dist/cli/commands/web.d.ts +0 -2
- package/dist/cli/commands/web.js +0 -286
- package/dist/interf-ui/404.html +0 -1
- package/dist/interf-ui/__next.__PAGE__.txt +0 -10
- package/dist/interf-ui/__next._full.txt +0 -20
- package/dist/interf-ui/__next._head.txt +0 -5
- package/dist/interf-ui/__next._index.txt +0 -5
- package/dist/interf-ui/__next._tree.txt +0 -5
- package/dist/interf-ui/_next/static/--reS3xBzM5zc6QxNjZd6/_buildManifest.js +0 -11
- package/dist/interf-ui/_next/static/--reS3xBzM5zc6QxNjZd6/_clientMiddlewareManifest.js +0 -1
- package/dist/interf-ui/_next/static/--reS3xBzM5zc6QxNjZd6/_ssgManifest.js +0 -1
- package/dist/interf-ui/_next/static/chunks/0.tjb6f4golw..css +0 -3
- package/dist/interf-ui/_next/static/chunks/03~yq9q893hmn.js +0 -1
- package/dist/interf-ui/_next/static/chunks/085-n_jv2ng_q.css +0 -1
- package/dist/interf-ui/_next/static/chunks/0dn41fa_zvgsl.js +0 -1
- package/dist/interf-ui/_next/static/chunks/0g-ea0zj5d-0k.js +0 -1
- package/dist/interf-ui/_next/static/chunks/0gwqglc4iz583.js +0 -1
- package/dist/interf-ui/_next/static/chunks/0haldgm65ve6l.js +0 -1
- package/dist/interf-ui/_next/static/chunks/0nv3am99vjzn4.js +0 -1
- package/dist/interf-ui/_next/static/chunks/0s77gt_o4jwtx.js +0 -1
- package/dist/interf-ui/_next/static/chunks/0y5z3t-z1c8ks.js.map +0 -5
- package/dist/interf-ui/_next/static/chunks/0~a36ujuzpaz..js +0 -116
- package/dist/interf-ui/_next/static/chunks/10jeodxe4nkgj.js +0 -31
- package/dist/interf-ui/_next/static/chunks/119h2rouych2t.js +0 -1
- package/dist/interf-ui/_next/static/chunks/13c8b~m8knjsf.js +0 -1
- package/dist/interf-ui/_next/static/chunks/14dznb2qpt-ho.js +0 -91
- package/dist/interf-ui/_next/static/chunks/15z_en80lrq-3.js +0 -5
- package/dist/interf-ui/_next/static/chunks/turbopack-0p.pvcjrtq-jh.js +0 -1
- package/dist/interf-ui/_next/static/chunks/turbopack-0usj_75.8frlw.js +0 -1
- package/dist/interf-ui/_next/static/chunks/turbopack-worker-0sjn--fhq~1cg.js +0 -1
- package/dist/interf-ui/_next/static/media/GeistMono_Variable.p.17jn9btb_52pq.woff2 +0 -0
- package/dist/interf-ui/_next/static/media/Geist_Variable-s.p.0-te~ja_gpvcf.woff2 +0 -0
- package/dist/interf-ui/_next/static/media/worker.102zas1s52_pf.js +0 -109
- package/dist/interf-ui/_not-found/__next._full.txt +0 -15
- package/dist/interf-ui/_not-found/__next._head.txt +0 -5
- package/dist/interf-ui/_not-found/__next._index.txt +0 -5
- package/dist/interf-ui/_not-found/__next._not-found.__PAGE__.txt +0 -5
- package/dist/interf-ui/_not-found/__next._not-found.txt +0 -5
- package/dist/interf-ui/_not-found/__next._tree.txt +0 -2
- package/dist/interf-ui/_not-found.html +0 -1
- package/dist/interf-ui/_not-found.txt +0 -15
- package/dist/interf-ui/index.html +0 -1
- package/dist/interf-ui/index.txt +0 -20
- package/dist/packages/contracts/lib/preparation-paths.d.ts +0 -117
- package/dist/packages/contracts/lib/preparation-paths.js +0 -177
- package/dist/packages/engine/action-definitions.d.ts +0 -407
- package/dist/packages/engine/action-definitions.js +0 -1158
- package/dist/packages/engine/action-values.d.ts +0 -1
- package/dist/packages/engine/action-values.js +0 -1
- package/dist/packages/engine/agents/lib/args.d.ts +0 -4
- package/dist/packages/engine/agents/lib/args.js +0 -52
- package/dist/packages/engine/agents/lib/chart-guidance.d.ts +0 -1
- package/dist/packages/engine/agents/lib/chart-guidance.js +0 -8
- package/dist/packages/engine/agents/lib/constants.js +0 -28
- package/dist/packages/engine/agents/lib/logs.d.ts +0 -2
- package/dist/packages/engine/agents/lib/logs.js +0 -17
- package/dist/packages/engine/agents/lib/render.d.ts +0 -8
- package/dist/packages/engine/agents/lib/schema.d.ts +0 -8
- package/dist/packages/engine/agents/lib/schema.js +0 -7
- package/dist/packages/engine/agents/lib/shells.d.ts +0 -74
- package/dist/packages/engine/agents/lib/shells.js +0 -1052
- package/dist/packages/engine/agents/lib/verifiable-context-bootstrap.d.ts +0 -3
- package/dist/packages/engine/build/artifact-counts.d.ts +0 -1
- package/dist/packages/engine/build/billing-events.d.ts +0 -89
- package/dist/packages/engine/build/build-pipeline.js +0 -175
- package/dist/packages/engine/build/build-plan-runs.d.ts +0 -14
- package/dist/packages/engine/build/build-plan-runs.js +0 -31
- package/dist/packages/engine/build/build-stage-plan.d.ts +0 -16
- package/dist/packages/engine/build/build-stage-plan.js +0 -100
- package/dist/packages/engine/build/build-stage-runner.js +0 -94
- package/dist/packages/engine/build/build-target.js +0 -16
- package/dist/packages/engine/build/check-evaluator.js +0 -298
- package/dist/packages/engine/build/lib/schema.js +0 -316
- package/dist/packages/engine/build/reset.d.ts +0 -2
- package/dist/packages/engine/build/reset.js +0 -74
- package/dist/packages/engine/build/runtime-inventory.d.ts +0 -7
- package/dist/packages/engine/build/runtime-paths.d.ts +0 -8
- package/dist/packages/engine/build/runtime-paths.js +0 -26
- package/dist/packages/engine/build/runtime-runs.d.ts +0 -10
- package/dist/packages/engine/build/runtime-runs.js +0 -224
- package/dist/packages/engine/build/runtime.d.ts +0 -5
- package/dist/packages/engine/build/runtime.js +0 -4
- package/dist/packages/engine/build/source-files.d.ts +0 -46
- package/dist/packages/engine/build/source-files.js +0 -149
- package/dist/packages/engine/build/state-artifacts.d.ts +0 -9
- package/dist/packages/engine/build/state-artifacts.js +0 -14
- package/dist/packages/engine/build/state-health.d.ts +0 -4
- package/dist/packages/engine/build/state-io.d.ts +0 -11
- package/dist/packages/engine/build/state-io.js +0 -82
- package/dist/packages/engine/build/state-paths.d.ts +0 -5
- package/dist/packages/engine/build/state-paths.js +0 -16
- package/dist/packages/engine/build/state-view.d.ts +0 -5
- package/dist/packages/engine/build/state-view.js +0 -94
- package/dist/packages/engine/build/state.d.ts +0 -7
- package/dist/packages/engine/build/state.js +0 -12
- package/dist/packages/engine/build/validate-helpers.d.ts +0 -12
- package/dist/packages/engine/build/validate-helpers.js +0 -41
- package/dist/packages/engine/build/verifiable-context-paths.d.ts +0 -47
- package/dist/packages/engine/build/verifiable-context-paths.js +0 -121
- package/dist/packages/engine/build/verifiable-context-schema.d.ts +0 -21
- package/dist/packages/engine/build/verifiable-context-schema.js +0 -126
- package/dist/packages/engine/cloud-seams.d.ts +0 -115
- package/dist/packages/engine/cloud-seams.js +0 -84
- package/dist/packages/engine/index.d.ts +0 -22
- package/dist/packages/engine/index.js +0 -15
- package/dist/packages/engine/instance-paths.d.ts +0 -106
- package/dist/packages/engine/instance-paths.js +0 -171
- package/dist/packages/engine/lib/schema.d.ts +0 -6304
- package/dist/packages/engine/lib/schema.js +0 -730
- package/dist/packages/engine/native-run-handlers.d.ts +0 -25
- package/dist/packages/engine/preparation-store.d.ts +0 -105
- package/dist/packages/engine/preparation-store.js +0 -213
- package/dist/packages/engine/readiness-check-draft.d.ts +0 -20
- package/dist/packages/engine/readiness-check-draft.js +0 -111
- package/dist/packages/engine/requested-artifacts.d.ts +0 -5
- package/dist/packages/engine/routes.d.ts +0 -85
- package/dist/packages/engine/routes.js +0 -99
- package/dist/packages/engine/runtime-caches.d.ts +0 -76
- package/dist/packages/engine/runtime-caches.js +0 -191
- package/dist/packages/engine/runtime-proposal-helpers.d.ts +0 -35
- package/dist/packages/engine/runtime-proposal-helpers.js +0 -247
- package/dist/packages/engine/runtime.d.ts +0 -371
- package/dist/packages/engine/runtime.js +0 -2463
- package/dist/packages/engine/server.d.ts +0 -58
- package/dist/packages/engine/server.js +0 -1399
- package/dist/packages/engine/verify/readiness-check-run.d.ts +0 -82
- package/dist/packages/engine/verify/readiness-check-run.js +0 -265
- package/dist/packages/engine/verify/verify-paths.js +0 -61
- package/dist/packages/engine/verify/verify-sandbox.js +0 -88
- package/dist/packages/engine/verify/verify-specs.js +0 -114
- package/dist/packages/engine/verify/verify-targets.d.ts +0 -5
- package/dist/packages/engine/wire-schemas.d.ts +0 -547
- package/dist/packages/engine/wire-schemas.js +0 -59
- package/dist/packages/project/index.d.ts +0 -9
- package/dist/packages/project/interf-bootstrap.d.ts +0 -1
- package/dist/packages/project/interf-bootstrap.js +0 -1
- package/dist/packages/project/interf-scaffold.d.ts +0 -3
- package/dist/packages/project/interf-scaffold.js +0 -136
- package/dist/packages/project/interf.d.ts +0 -4
- package/dist/packages/project/interf.js +0 -3
- package/dist/packages/project/lib/schema.d.ts +0 -328
- package/dist/packages/project/lib/schema.js +0 -136
- package/dist/packages/project/preparation-entries.d.ts +0 -11
- package/dist/packages/project/preparation-entries.js +0 -49
- package/dist/packages/project/source-config.d.ts +0 -46
- package/dist/packages/project/source-config.js +0 -394
- package/dist/packages/project/source-folders.d.ts +0 -11
- package/public-repo/build-plans/interf-default/build/stages/shape/SKILL.md +0 -27
- package/public-repo/build-plans/interf-default/build/stages/structure/SKILL.md +0 -21
- package/public-repo/plugins/README.md +0 -9
- package/public-repo/plugins/interf/.claude-plugin/plugin.json +0 -21
- package/public-repo/plugins/interf/.mcp.json +0 -12
- package/public-repo/plugins/interf/README.md +0 -32
- package/public-repo/plugins/interf/skills/interf/SKILL.md +0 -376
- /package/dist/packages/{engine/agents/lib/types.js → runtime/actions/fields.js} +0 -0
- /package/dist/packages/{engine → runtime}/agents/lib/agents.d.ts +0 -0
- /package/dist/packages/{engine → runtime}/agents/lib/agents.js +0 -0
- /package/dist/packages/{engine → runtime}/agents/lib/execution.d.ts +0 -0
- /package/dist/packages/{engine → runtime}/agents/lib/preflight.d.ts +0 -0
- /package/dist/packages/{engine → runtime}/agents/lib/status.d.ts +0 -0
- /package/dist/packages/{engine/build/runtime-types.js → runtime/agents/lib/types.js} +0 -0
- /package/dist/packages/{engine/verify/verify-types.js → runtime/agents/providers/types.js} +0 -0
- /package/dist/packages/{engine → runtime}/agents/role-router.d.ts +0 -0
- /package/dist/packages/{engine → runtime}/build/build-execution.d.ts +0 -0
- /package/dist/packages/{engine → runtime}/build/build-execution.js +0 -0
- /package/dist/packages/{engine → runtime}/build/runtime-contracts.d.ts +0 -0
- /package/dist/packages/{engine → runtime}/build/runtime-prompt.d.ts +0 -0
- /package/dist/packages/{engine → runtime}/execution/adapters.d.ts +0 -0
- /package/dist/packages/{engine → runtime}/execution/adapters.js +0 -0
- /package/dist/packages/{engine → runtime}/execution/events.d.ts +0 -0
- /package/dist/packages/{engine → runtime}/execution/events.js +0 -0
- /package/dist/packages/{engine → runtime}/verify/verify-profile-presets.d.ts +0 -0
- /package/dist/packages/{engine → runtime}/verify/verify-profile-presets.js +0 -0
- /package/dist/packages/{engine → runtime}/verify/verify-types.d.ts +0 -0
|
@@ -0,0 +1,1226 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
3
|
+
import { listFilesRecursive } from "../../contracts/utils/filesystem.js";
|
|
4
|
+
import { parseJsonFrontmatter } from "../../contracts/utils/parse.js";
|
|
5
|
+
import { CANONICAL_LAYER_DIRS, HOME_SPINE_FILE, graphRelativePathPattern, } from "../../contracts/lib/context-graph-layer.js";
|
|
6
|
+
import { CheckKindSchema, SourceManifestSchema, } from "../../contracts/lib/schema.js";
|
|
7
|
+
import { countBrokenWikilinks, isOutputMarkdownFile, validateSynthFiles, } from "./validate.js";
|
|
8
|
+
/**
|
|
9
|
+
* Build a CheckResult envelope from an evaluator outcome. Centralizes the
|
|
10
|
+
* timestamp + required-flag plumbing so individual evaluators can
|
|
11
|
+
* focus on the pass/fail decision and a one-line summary.
|
|
12
|
+
*/
|
|
13
|
+
function makeCheckResult(check, passed, summary, details) {
|
|
14
|
+
return {
|
|
15
|
+
check_id: check.id,
|
|
16
|
+
kind: check.kind,
|
|
17
|
+
passed,
|
|
18
|
+
required: check.required,
|
|
19
|
+
summary,
|
|
20
|
+
...(details !== undefined ? { details } : {}),
|
|
21
|
+
evaluated_at: new Date().toISOString(),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function resolveTargetPath(check, context) {
|
|
25
|
+
const target = context.targetPath;
|
|
26
|
+
if (!target)
|
|
27
|
+
return null;
|
|
28
|
+
const root = resolve(context.rootPath);
|
|
29
|
+
const absolute = resolve(root, target);
|
|
30
|
+
// Defense in depth — schema-level validation already rejects path
|
|
31
|
+
// traversal, but a hand-edited Build Plan or stale on-disk fixture could
|
|
32
|
+
// slip through. Reject anything that escapes the root.
|
|
33
|
+
if (absolute !== root && !absolute.startsWith(`${root}/`)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return absolute;
|
|
37
|
+
}
|
|
38
|
+
function listMarkdownFiles(absolutePath) {
|
|
39
|
+
if (!existsSync(absolutePath))
|
|
40
|
+
return [];
|
|
41
|
+
try {
|
|
42
|
+
const stats = statSync(absolutePath);
|
|
43
|
+
if (stats.isFile()) {
|
|
44
|
+
return isOutputMarkdownFile(absolutePath) ? [absolutePath] : [];
|
|
45
|
+
}
|
|
46
|
+
if (stats.isDirectory()) {
|
|
47
|
+
return listFilesRecursive(absolutePath, isOutputMarkdownFile);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
function countFiles(absolutePath) {
|
|
56
|
+
if (!existsSync(absolutePath))
|
|
57
|
+
return 0;
|
|
58
|
+
try {
|
|
59
|
+
const stats = statSync(absolutePath);
|
|
60
|
+
if (stats.isFile())
|
|
61
|
+
return 1;
|
|
62
|
+
if (stats.isDirectory()) {
|
|
63
|
+
return listFilesRecursive(absolutePath, () => true).length;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
function countSourceSummaryFolders(absolutePath, options) {
|
|
72
|
+
if (!existsSync(absolutePath))
|
|
73
|
+
return { actual: 0, missingSummary: [] };
|
|
74
|
+
try {
|
|
75
|
+
const stats = statSync(absolutePath);
|
|
76
|
+
if (!stats.isDirectory())
|
|
77
|
+
return { actual: 0, missingSummary: [] };
|
|
78
|
+
const missingSummary = [];
|
|
79
|
+
let actual = 0;
|
|
80
|
+
for (const entry of readdirSync(absolutePath, { withFileTypes: true })) {
|
|
81
|
+
if (!entry.isDirectory())
|
|
82
|
+
continue;
|
|
83
|
+
const dirPath = join(absolutePath, entry.name);
|
|
84
|
+
const hasSummary = existsSync(join(dirPath, options.summaryFile));
|
|
85
|
+
const hasManifest = existsSync(join(dirPath, options.manifestFile));
|
|
86
|
+
if (hasSummary || hasManifest) {
|
|
87
|
+
actual += 1;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
missingSummary.push(entry.name);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { actual, missingSummary };
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return { actual: 0, missingSummary: [] };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function summaryDirectoriesWithEvidence(absolutePath, options) {
|
|
100
|
+
const dirs = new Set();
|
|
101
|
+
if (!existsSync(absolutePath))
|
|
102
|
+
return dirs;
|
|
103
|
+
for (const file of listFilesRecursive(absolutePath, () => true)) {
|
|
104
|
+
const name = basename(file);
|
|
105
|
+
if (name !== options.summaryFile && name !== options.manifestFile)
|
|
106
|
+
continue;
|
|
107
|
+
dirs.add(relative(absolutePath, dirname(file)).replaceAll("\\", "/"));
|
|
108
|
+
}
|
|
109
|
+
return dirs;
|
|
110
|
+
}
|
|
111
|
+
function sourceSummaryFolderCandidates(sourcePath, sourceFileId, basenameCounts) {
|
|
112
|
+
const normalized = sourcePath.replaceAll("\\", "/");
|
|
113
|
+
const base = basename(normalized);
|
|
114
|
+
const candidates = [
|
|
115
|
+
normalized,
|
|
116
|
+
sourceFileId,
|
|
117
|
+
basenameCounts.get(base) === 1 ? base : "",
|
|
118
|
+
].filter((candidate) => candidate.length > 0);
|
|
119
|
+
return [...new Set(candidates)];
|
|
120
|
+
}
|
|
121
|
+
function sourceSummaryCoverage(absolutePath, manifest, options) {
|
|
122
|
+
const evidenceDirs = summaryDirectoriesWithEvidence(absolutePath, options);
|
|
123
|
+
const matchedDirs = new Set();
|
|
124
|
+
const missingSourcePaths = [];
|
|
125
|
+
const basenameCounts = new Map();
|
|
126
|
+
for (const sourceFile of manifest.files) {
|
|
127
|
+
const base = basename(sourceFile.path.replaceAll("\\", "/"));
|
|
128
|
+
basenameCounts.set(base, (basenameCounts.get(base) ?? 0) + 1);
|
|
129
|
+
}
|
|
130
|
+
for (const sourceFile of manifest.files) {
|
|
131
|
+
const candidates = sourceSummaryFolderCandidates(sourceFile.path, sourceFile.id, basenameCounts);
|
|
132
|
+
const matched = candidates.find((candidate) => evidenceDirs.has(candidate));
|
|
133
|
+
if (matched) {
|
|
134
|
+
matchedDirs.add(matched);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
missingSourcePaths.push(sourceFile.path);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
actual: manifest.files.length - missingSourcePaths.length,
|
|
142
|
+
expected: manifest.source_total,
|
|
143
|
+
missingSourcePaths,
|
|
144
|
+
extraSummaryFolders: [...evidenceDirs].filter((dir) => !matchedDirs.has(dir)).sort((left, right) => left.localeCompare(right)),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const WIKILINK_TARGET_PATTERN = /\[\[([^[\]\n]+)\]\]/g;
|
|
148
|
+
const MARKDOWN_LINK_TARGET_PATTERN = /!?\[[^\]\n]*\]\(([^)\n]+)\)/g;
|
|
149
|
+
function normalizeGraphPath(value) {
|
|
150
|
+
return value.replaceAll("\\", "/").replace(/^\.\/+/, "").replace(/\.md$/i, "").replace(/\/+$/g, "");
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Like normalizeGraphPath but preserves a trailing `.md` — summary folders are
|
|
154
|
+
* named after the source file and legitimately end in `.md` (e.g.
|
|
155
|
+
* `summaries/meeting-notes.md/`). Stripping it would split the folder identity.
|
|
156
|
+
*/
|
|
157
|
+
function normalizeFolderPath(value) {
|
|
158
|
+
return value.replaceAll("\\", "/").replace(/^\.\/+/, "").replace(/\/+$/g, "");
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Wikilink targets referenced by a single note, normalized to graph-relative
|
|
162
|
+
* basenames (no `.md`, no `#anchor`, no `|alias`). Source-agnostic: reads only
|
|
163
|
+
* the `[[...]]` syntax, never a task taxonomy.
|
|
164
|
+
*/
|
|
165
|
+
function noteWikilinkTargets(content) {
|
|
166
|
+
const targets = [];
|
|
167
|
+
for (const match of content.matchAll(WIKILINK_TARGET_PATTERN)) {
|
|
168
|
+
const raw = match[1]?.split("|")[0]?.split("#")[0]?.trim();
|
|
169
|
+
if (raw)
|
|
170
|
+
targets.push(normalizeGraphPath(raw));
|
|
171
|
+
}
|
|
172
|
+
return targets;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Every link token a note references, across the three link forms the
|
|
176
|
+
* StageManifest's `parseLinks` reads: `[[wikilinks]]`, `[markdown](relative.md)`
|
|
177
|
+
* links (http(s) excluded), and bare graph-relative path mentions in body text
|
|
178
|
+
* (`knowledge/foo`, via the shared `graphRelativePathPattern` so the scanner and
|
|
179
|
+
* the layer model can never list different folders). Each token is normalized to
|
|
180
|
+
* a graph-relative path with no `.md`, anchor, or alias. Reusing the same link
|
|
181
|
+
* surface the manifest uses is what keeps the Check and the manifest rollup in
|
|
182
|
+
* lockstep on which notes are web-connected.
|
|
183
|
+
*/
|
|
184
|
+
function noteLinkTargets(content) {
|
|
185
|
+
const targets = new Set();
|
|
186
|
+
for (const target of noteWikilinkTargets(content))
|
|
187
|
+
targets.add(target);
|
|
188
|
+
for (const match of content.matchAll(MARKDOWN_LINK_TARGET_PATTERN)) {
|
|
189
|
+
const raw = match[1]?.split("#")[0]?.trim();
|
|
190
|
+
if (!raw || /^https?:\/\//i.test(raw))
|
|
191
|
+
continue;
|
|
192
|
+
targets.add(normalizeGraphPath(raw));
|
|
193
|
+
}
|
|
194
|
+
for (const match of content.matchAll(graphRelativePathPattern())) {
|
|
195
|
+
const raw = match[1]?.split("#")[0]?.trim().replace(/[),.;:]+$/g, "");
|
|
196
|
+
if (raw)
|
|
197
|
+
targets.add(normalizeGraphPath(raw));
|
|
198
|
+
}
|
|
199
|
+
return [...targets].filter((target) => target.length > 0);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Resolve a single link token to the knowledge note it names, or report that it
|
|
203
|
+
* is ambiguous. Mirrors the basename-aliasing discipline `existingSummaryFolderSet`
|
|
204
|
+
* already uses in this file: a full graph-path always resolves (paths are unique
|
|
205
|
+
* within the subtree); a token ending `/<basename>` resolves to the unique note
|
|
206
|
+
* with that path suffix; a BARE basename resolves ONLY when exactly one note
|
|
207
|
+
* carries it. A bare basename two or more notes share (e.g. `[[claim]]` for both
|
|
208
|
+
* `topics/claim` and `entities/claim`) is `ambiguous` — it must NOT credit a web
|
|
209
|
+
* edge to one arbitrarily, which would silently connect the linker and hide the
|
|
210
|
+
* other namesake's island. Returns the matched note, `ambiguous`, or `null`.
|
|
211
|
+
*/
|
|
212
|
+
function resolveLinkToNote(token, byGraphPath, byBasename, ambiguousBasenames) {
|
|
213
|
+
const clean = normalizeGraphPath(token);
|
|
214
|
+
if (clean.length === 0)
|
|
215
|
+
return null;
|
|
216
|
+
const exact = byGraphPath.get(clean);
|
|
217
|
+
if (exact)
|
|
218
|
+
return exact;
|
|
219
|
+
if (clean.includes("/")) {
|
|
220
|
+
// A path-shaped token: credit a note whose full path is the token's trailing
|
|
221
|
+
// segments. Unique by construction (graph paths are unique), so no ambiguity.
|
|
222
|
+
const suffix = basename(clean);
|
|
223
|
+
for (const note of byGraphPath.values()) {
|
|
224
|
+
if (clean.endsWith(`/${note.base}`) && note.base === suffix && clean.endsWith(note.graphPath)) {
|
|
225
|
+
return note;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
// A bare basename: resolves only when unambiguous; a shared basename is a gap.
|
|
231
|
+
if (ambiguousBasenames.has(clean))
|
|
232
|
+
return "ambiguous";
|
|
233
|
+
return byBasename.get(clean) ?? null;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Resolve a note-relative link token (`./x`, `../x`) against the linking note's
|
|
237
|
+
* directory into an absolute graph path, so a relative wikilink credits a web
|
|
238
|
+
* edge the same way the wikilink validator resolves it. Without this, a note
|
|
239
|
+
* that links `[[../launch]]` is falsely scored a disconnected island. Non-relative
|
|
240
|
+
* tokens (full graph paths, bare basenames) are returned untouched for the global
|
|
241
|
+
* lookup.
|
|
242
|
+
*/
|
|
243
|
+
function resolveRelativeGraphPath(fromGraphPath, token) {
|
|
244
|
+
const lastSlash = fromGraphPath.lastIndexOf("/");
|
|
245
|
+
const segments = lastSlash >= 0 ? fromGraphPath.slice(0, lastSlash).split("/") : [];
|
|
246
|
+
for (const part of token.split("/")) {
|
|
247
|
+
if (part === "" || part === ".")
|
|
248
|
+
continue;
|
|
249
|
+
if (part === "..") {
|
|
250
|
+
segments.pop();
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
segments.push(part);
|
|
254
|
+
}
|
|
255
|
+
return normalizeGraphPath(segments.join("/"));
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Markdown notes that form the CONTENT of a Context Graph — the canonical content
|
|
259
|
+
* layers (`summaries/`, `knowledge/`, `artifacts/`) plus the `home.md` spine —
|
|
260
|
+
* resolved to absolute file paths. Deliberately EXCLUDES the runtime scaffolding
|
|
261
|
+
* that also lives under the graph root (`.interf/`, `.claude/`, `.agents/`,
|
|
262
|
+
* skill `SKILL.md` docs, `CLAUDE.md`, `AGENTS.md`, view specs): those are not
|
|
263
|
+
* graph notes and must never be scored for web connectivity — they are always
|
|
264
|
+
* "islands" and several share the basename `SKILL`, which would inject false
|
|
265
|
+
* ambiguity. This mirrors the dot-entry skipping the semantic-graph builder
|
|
266
|
+
* already does, and keys off the central `CANONICAL_LAYER_DIRS` / `HOME_SPINE_FILE`
|
|
267
|
+
* so the connectivity floor and the layer model never list different folders.
|
|
268
|
+
*/
|
|
269
|
+
function collectGraphContentNotes(graphRoot) {
|
|
270
|
+
const files = [];
|
|
271
|
+
for (const layer of CANONICAL_LAYER_DIRS) {
|
|
272
|
+
files.push(...listMarkdownFiles(join(graphRoot, layer)));
|
|
273
|
+
}
|
|
274
|
+
const home = join(graphRoot, HOME_SPINE_FILE);
|
|
275
|
+
if (existsSync(home) && isOutputMarkdownFile(home))
|
|
276
|
+
files.push(home);
|
|
277
|
+
return files;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Note-web connectivity over a pre-collected note set — the filesystem-side
|
|
281
|
+
* mirror of the StageManifest's `knowledgeWebConnectivity`. Each file in `noteFiles`
|
|
282
|
+
* IS a note in the web (the caller chooses the scope and the file set: the
|
|
283
|
+
* knowledge layer for `knowledge_web_connectivity`, the canonical CONTENT layers
|
|
284
|
+
* for `graph_notes_connected`), so no layer re-derivation is needed here. A note
|
|
285
|
+
* is web-connected when it links a DIFFERENT note in the same set OR is linked
|
|
286
|
+
* by one (UNDIRECTED, degree ≥ 1); degree 0 is a disconnected island.
|
|
287
|
+
* Connectedness, not a count. Vacuous pass: ≤ 1 note cannot form a web, so it is
|
|
288
|
+
* reported connected with no islands. Edges and matching reuse the same link
|
|
289
|
+
* surface and basename-aliasing rule the manifest and the backlink check use, so
|
|
290
|
+
* the gates agree on islands.
|
|
291
|
+
*/
|
|
292
|
+
function analyzeNoteWeb(noteFiles, context) {
|
|
293
|
+
const root = resolve(context.rootPath);
|
|
294
|
+
const notes = noteFiles.map((file) => {
|
|
295
|
+
const graphPath = normalizeGraphPath(relative(root, file).replaceAll("\\", "/"));
|
|
296
|
+
return { file, graphPath, base: basename(graphPath) };
|
|
297
|
+
});
|
|
298
|
+
if (notes.length <= 1) {
|
|
299
|
+
return { notes: notes.length, connected: notes.length, islands: [], ambiguousLinkNotes: [] };
|
|
300
|
+
}
|
|
301
|
+
// Full path always keys a note; a basename keys a note only when unique. A
|
|
302
|
+
// basename two or more notes share is ambiguous and never resolves a bare link.
|
|
303
|
+
const byGraphPath = new Map();
|
|
304
|
+
const basenameCounts = new Map();
|
|
305
|
+
for (const note of notes) {
|
|
306
|
+
byGraphPath.set(note.graphPath, note);
|
|
307
|
+
basenameCounts.set(note.base, (basenameCounts.get(note.base) ?? 0) + 1);
|
|
308
|
+
}
|
|
309
|
+
const byBasename = new Map();
|
|
310
|
+
const ambiguousBasenames = new Set();
|
|
311
|
+
for (const note of notes) {
|
|
312
|
+
if ((basenameCounts.get(note.base) ?? 0) > 1) {
|
|
313
|
+
ambiguousBasenames.add(note.base);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
byBasename.set(note.base, note);
|
|
317
|
+
}
|
|
318
|
+
const hasOutbound = new Set();
|
|
319
|
+
const hasInbound = new Set();
|
|
320
|
+
const ambiguousLinkNotes = new Set();
|
|
321
|
+
for (const from of notes) {
|
|
322
|
+
for (const token of noteLinkTargets(readFileSync(from.file, "utf8"))) {
|
|
323
|
+
const candidate = token.startsWith("../") || token.startsWith("./")
|
|
324
|
+
? resolveRelativeGraphPath(from.graphPath, token)
|
|
325
|
+
: token;
|
|
326
|
+
const resolved = resolveLinkToNote(candidate, byGraphPath, byBasename, ambiguousBasenames);
|
|
327
|
+
if (resolved === "ambiguous") {
|
|
328
|
+
ambiguousLinkNotes.add(from.graphPath);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (!resolved || resolved.graphPath === from.graphPath)
|
|
332
|
+
continue;
|
|
333
|
+
hasOutbound.add(from.graphPath);
|
|
334
|
+
hasInbound.add(resolved.graphPath);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const islands = notes
|
|
338
|
+
.filter((note) => !hasOutbound.has(note.graphPath) && !hasInbound.has(note.graphPath))
|
|
339
|
+
.map((note) => note.graphPath)
|
|
340
|
+
.sort((left, right) => left.localeCompare(right));
|
|
341
|
+
return {
|
|
342
|
+
notes: notes.length,
|
|
343
|
+
connected: notes.length - islands.length,
|
|
344
|
+
islands,
|
|
345
|
+
ambiguousLinkNotes: [...ambiguousLinkNotes].sort((left, right) => left.localeCompare(right)),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Source refs a note declares, drawn from frontmatter source keys and from any
|
|
350
|
+
* literal source path mentioned in the body. Mirrors the StageManifest reader so
|
|
351
|
+
* the check and the manifest agree on what a note "cites".
|
|
352
|
+
*/
|
|
353
|
+
function noteSourceRefs(content, frontmatter, knownSourcePaths) {
|
|
354
|
+
const refs = new Set();
|
|
355
|
+
for (const key of ["source_refs", "source_ref", "source_path", "source"]) {
|
|
356
|
+
const value = frontmatter[key];
|
|
357
|
+
if (typeof value === "string" && value.trim().length > 0)
|
|
358
|
+
refs.add(value.trim());
|
|
359
|
+
if (Array.isArray(value)) {
|
|
360
|
+
for (const entry of value) {
|
|
361
|
+
if (typeof entry === "string" && entry.trim().length > 0)
|
|
362
|
+
refs.add(entry.trim());
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
for (const sourcePath of knownSourcePaths) {
|
|
367
|
+
if (content.includes(sourcePath))
|
|
368
|
+
refs.add(sourcePath);
|
|
369
|
+
}
|
|
370
|
+
return [...refs];
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Resolve which summary folders actually exist on disk (folders under the
|
|
374
|
+
* summaries directory that hold a summary or manifest file). Returns a lookup
|
|
375
|
+
* from any reasonable reference form — the folder's graph-relative path and its
|
|
376
|
+
* basename — to the canonical folder path, plus the set of basenames that more
|
|
377
|
+
* than one folder carries. Reuses the same summary/manifest evidence convention
|
|
378
|
+
* the summary-coverage check uses.
|
|
379
|
+
*
|
|
380
|
+
* A basename alias is registered ONLY when that basename is unambiguous across
|
|
381
|
+
* summary folders. When two folders share a basename (e.g. `dept/report.pdf`
|
|
382
|
+
* and `legal/report.pdf` both basename `report.pdf`), aliasing one of them
|
|
383
|
+
* would silently resolve a basename-only ref to the wrong folder and HIDE a
|
|
384
|
+
* real orphan. Ambiguous basenames are left out of the `lookup` so a
|
|
385
|
+
* basename-only ref to them never resolves to one arbitrary folder; the
|
|
386
|
+
* `ambiguousBasenames` set lets the caller treat such a ref as an unresolved
|
|
387
|
+
* gap to surface rather than a silent pass. Full-path refs always resolve
|
|
388
|
+
* regardless of basename collisions.
|
|
389
|
+
*/
|
|
390
|
+
function existingSummaryFolderSet(summariesAbsolutePath, options) {
|
|
391
|
+
const lookup = new Map();
|
|
392
|
+
const evidenceDirs = summaryDirectoriesWithEvidence(summariesAbsolutePath, options);
|
|
393
|
+
const folders = [];
|
|
394
|
+
const basenameCounts = new Map();
|
|
395
|
+
for (const dir of evidenceDirs) {
|
|
396
|
+
const folder = normalizeFolderPath(dir);
|
|
397
|
+
if (folder.length === 0)
|
|
398
|
+
continue;
|
|
399
|
+
folders.push(folder);
|
|
400
|
+
// Full-path key always wins; it is unique per folder.
|
|
401
|
+
lookup.set(folder, folder);
|
|
402
|
+
const base = basename(folder);
|
|
403
|
+
if (base)
|
|
404
|
+
basenameCounts.set(base, (basenameCounts.get(base) ?? 0) + 1);
|
|
405
|
+
}
|
|
406
|
+
const ambiguousBasenames = new Set();
|
|
407
|
+
// Second pass: alias a basename to its folder only when exactly one folder
|
|
408
|
+
// carries that basename. Skip any basename that already collides with a
|
|
409
|
+
// full-path key (a folder literally named like another folder's basename) —
|
|
410
|
+
// the path key must not be shadowed.
|
|
411
|
+
for (const folder of folders) {
|
|
412
|
+
const base = basename(folder);
|
|
413
|
+
if (!base || base === folder)
|
|
414
|
+
continue;
|
|
415
|
+
if ((basenameCounts.get(base) ?? 0) > 1) {
|
|
416
|
+
ambiguousBasenames.add(base);
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
if (lookup.has(base))
|
|
420
|
+
continue;
|
|
421
|
+
lookup.set(base, folder);
|
|
422
|
+
}
|
|
423
|
+
return { lookup, ambiguousBasenames };
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Whether one normalized graph path contains another at a path-segment (or
|
|
427
|
+
* trailing-delimiter) boundary, not mid-segment. `decks/q3.pptx` contains
|
|
428
|
+
* `decks/q3.pptx/pages/3` and `decks/q3.pptx#page=25`, but `decks/q3.pptx` does
|
|
429
|
+
* NOT contain `decks/q3.pptx-archive` — the suffix must begin at a `/` or `#`
|
|
430
|
+
* boundary. Source-agnostic: pure string-shape, no task taxonomy.
|
|
431
|
+
*/
|
|
432
|
+
function containsAtBoundary(container, inner) {
|
|
433
|
+
if (inner.length === 0 || container.length < inner.length)
|
|
434
|
+
return false;
|
|
435
|
+
const index = container.indexOf(inner);
|
|
436
|
+
if (index < 0)
|
|
437
|
+
return false;
|
|
438
|
+
// Inner must start at a segment boundary (path start or right after a "/").
|
|
439
|
+
if (index > 0 && container[index - 1] !== "/")
|
|
440
|
+
return false;
|
|
441
|
+
// Inner must end at a segment boundary (path end, or before a "/" / "#"
|
|
442
|
+
// anchor). Anything else (e.g. "q3.pptx" inside "q3.pptx-archive") is a
|
|
443
|
+
// mid-segment false match.
|
|
444
|
+
const after = container[index + inner.length];
|
|
445
|
+
return after === undefined || after === "/" || after === "#";
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Map an arbitrary source ref to the summary-folder names that could hold its
|
|
449
|
+
* summary. Generic across any Source: a ref like `decks/q3.pptx` matches the
|
|
450
|
+
* `decks/q3.pptx` folder, the basename `q3.pptx` (when unambiguous), or the
|
|
451
|
+
* manifest file id. Never hardcodes a task or filename.
|
|
452
|
+
*/
|
|
453
|
+
function summaryFolderCandidatesForRef(ref, manifest, basenameCounts) {
|
|
454
|
+
const normalizedRef = ref.replaceAll("\\", "/");
|
|
455
|
+
const normalized = normalizeFolderPath(ref).replace(/^summaries\//, "");
|
|
456
|
+
const candidates = new Set();
|
|
457
|
+
candidates.add(normalized);
|
|
458
|
+
const base = basename(normalized);
|
|
459
|
+
if (base)
|
|
460
|
+
candidates.add(base);
|
|
461
|
+
// Resolve through the Source Manifest so a page-level or partial ref still
|
|
462
|
+
// points at the file-level summary folder. Match only at path-segment
|
|
463
|
+
// boundaries: a bare substring test over-matches (`q3` would hit
|
|
464
|
+
// `q3-archive`), crediting the wrong summary and hiding a real orphan.
|
|
465
|
+
for (const file of manifest?.files ?? []) {
|
|
466
|
+
const filePath = file.path.replaceAll("\\", "/");
|
|
467
|
+
if (containsAtBoundary(normalizedRef, filePath) ||
|
|
468
|
+
containsAtBoundary(filePath, normalized) ||
|
|
469
|
+
containsAtBoundary(normalized, filePath)) {
|
|
470
|
+
candidates.add(filePath);
|
|
471
|
+
if (basenameCounts.get(basename(filePath)) === 1)
|
|
472
|
+
candidates.add(basename(filePath));
|
|
473
|
+
candidates.add(file.id);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return [...candidates].filter((candidate) => candidate.length > 0);
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Orphaned-summary analysis for a knowledge-style layer: a summary folder that is
|
|
480
|
+
* cited by some note's source_refs but is wikilinked by no note in the layer.
|
|
481
|
+
* Fully generic — derives expected backlinks from the notes' own refs and the
|
|
482
|
+
* Source Manifest, with no project-specific or task-specific input.
|
|
483
|
+
*/
|
|
484
|
+
function analyzeSummaryBacklinks(layerDir, context, options) {
|
|
485
|
+
const root = resolve(context.rootPath);
|
|
486
|
+
const manifest = loadSourceManifestForCheck(context).manifest;
|
|
487
|
+
const knownSourcePaths = manifest?.files.map((file) => file.path.replaceAll("\\", "/")) ?? [];
|
|
488
|
+
const basenameCounts = new Map();
|
|
489
|
+
for (const path of knownSourcePaths) {
|
|
490
|
+
const base = basename(path);
|
|
491
|
+
basenameCounts.set(base, (basenameCounts.get(base) ?? 0) + 1);
|
|
492
|
+
}
|
|
493
|
+
// Summary folders that actually exist on disk. You can only orphan a summary
|
|
494
|
+
// that exists — a note citing a source with no summary folder is not a
|
|
495
|
+
// backlink violation (there is nothing to link to). The lookup keys a folder
|
|
496
|
+
// by its full path and, only when unambiguous, its basename; ambiguousBasenames
|
|
497
|
+
// names the basenames carried by more than one folder.
|
|
498
|
+
const { lookup: existingSummaryFolders, ambiguousBasenames } = existingSummaryFolderSet(join(root, options.summariesDir), { summaryFile: options.summaryFile, manifestFile: options.manifestFile });
|
|
499
|
+
// Resolve a ref to a canonical summary folder. Three outcomes:
|
|
500
|
+
// "resolved" — a candidate matched an existing folder.
|
|
501
|
+
// "ambiguous" — nothing matched, but a basename candidate collides with two
|
|
502
|
+
// or more existing folders, so we refuse to pick one. This is a
|
|
503
|
+
// gap to surface, not a clean miss.
|
|
504
|
+
// "absent" — nothing matched and no summary folder exists for the ref.
|
|
505
|
+
const resolveCitedFolder = (ref) => {
|
|
506
|
+
const candidates = summaryFolderCandidatesForRef(ref, manifest, basenameCounts);
|
|
507
|
+
for (const candidate of candidates) {
|
|
508
|
+
const folder = existingSummaryFolders.get(candidate);
|
|
509
|
+
if (folder)
|
|
510
|
+
return { kind: "resolved", folder };
|
|
511
|
+
}
|
|
512
|
+
if (candidates.some((candidate) => ambiguousBasenames.has(candidate))) {
|
|
513
|
+
return { kind: "ambiguous" };
|
|
514
|
+
}
|
|
515
|
+
return { kind: "absent" };
|
|
516
|
+
};
|
|
517
|
+
// Set of every summary-folder backlink wikilink target present anywhere in the
|
|
518
|
+
// layer, normalized to the folder name (drop the trailing summary/manifest leaf).
|
|
519
|
+
const linkedSummaries = new Set();
|
|
520
|
+
const summaryLeaf = normalizeGraphPath(options.summaryFile);
|
|
521
|
+
const manifestLeaf = normalizeGraphPath(options.manifestFile);
|
|
522
|
+
const citedSummaryToNotes = new Map();
|
|
523
|
+
const notesWithUnlinkedCitations = new Set();
|
|
524
|
+
const notesWithAmbiguousCitations = new Set();
|
|
525
|
+
let notesScanned = 0;
|
|
526
|
+
const notes = listMarkdownFiles(layerDir);
|
|
527
|
+
// First pass: every existing summary folder linked anywhere in the layer,
|
|
528
|
+
// resolved to its canonical folder name so basename and path links agree. A
|
|
529
|
+
// wikilink is a concrete graph path, so it credits a backlink ONLY when the
|
|
530
|
+
// target resolves to a real summary folder. `existingSummaryFolders` already
|
|
531
|
+
// keys both each folder's full path and its basename (the latter only when
|
|
532
|
+
// unambiguous), so a legitimate bare-basename link still resolves. We do NOT
|
|
533
|
+
// fall back to a separate basename(folderRef) lookup: that would let a broken
|
|
534
|
+
// wikilink — one whose folder path does not exist (e.g.
|
|
535
|
+
// `[[summaries/typo/q3.pptx/summary]]`) — launder through its basename and
|
|
536
|
+
// wrongly credit an unrelated namesake folder, hiding a real orphan.
|
|
537
|
+
for (const note of notes) {
|
|
538
|
+
const content = readFileSync(note, "utf8");
|
|
539
|
+
for (const target of noteWikilinkTargets(content)) {
|
|
540
|
+
const withinSummaries = target.startsWith(`${options.summariesDir}/`)
|
|
541
|
+
? target.slice(options.summariesDir.length + 1)
|
|
542
|
+
: null;
|
|
543
|
+
if (withinSummaries === null)
|
|
544
|
+
continue;
|
|
545
|
+
const segments = withinSummaries.split("/");
|
|
546
|
+
const leaf = segments[segments.length - 1] ?? "";
|
|
547
|
+
const folderRef = leaf === summaryLeaf || leaf === manifestLeaf
|
|
548
|
+
? segments.slice(0, -1).join("/")
|
|
549
|
+
: withinSummaries;
|
|
550
|
+
const folder = existingSummaryFolders.get(folderRef);
|
|
551
|
+
if (folder)
|
|
552
|
+
linkedSummaries.add(folder);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
// Second pass: every existing summary a note cites, and whether it is linked.
|
|
556
|
+
for (const note of notes) {
|
|
557
|
+
notesScanned += 1;
|
|
558
|
+
const content = readFileSync(note, "utf8");
|
|
559
|
+
const parsed = parseJsonFrontmatter(content);
|
|
560
|
+
const frontmatter = parsed?.frontmatter ?? {};
|
|
561
|
+
const refs = noteSourceRefs(content, frontmatter, knownSourcePaths);
|
|
562
|
+
if (refs.length === 0)
|
|
563
|
+
continue;
|
|
564
|
+
const noteRel = relative(root, note).replaceAll("\\", "/");
|
|
565
|
+
let noteHasUnlinked = false;
|
|
566
|
+
let noteHasAmbiguous = false;
|
|
567
|
+
for (const ref of refs) {
|
|
568
|
+
const resolution = resolveCitedFolder(ref);
|
|
569
|
+
// An ambiguous basename-only citation cannot be proven backlinked; treat it
|
|
570
|
+
// as a surfaced gap rather than skipping it (which would hide the orphan).
|
|
571
|
+
if (resolution.kind === "ambiguous") {
|
|
572
|
+
noteHasAmbiguous = true;
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
// Only an existing summary folder can be orphaned; skip refs that point at
|
|
576
|
+
// a source with no summary in this graph.
|
|
577
|
+
if (resolution.kind === "absent")
|
|
578
|
+
continue;
|
|
579
|
+
const folder = resolution.folder;
|
|
580
|
+
const linked = linkedSummaries.has(folder);
|
|
581
|
+
const list = citedSummaryToNotes.get(folder) ?? [];
|
|
582
|
+
list.push(noteRel);
|
|
583
|
+
citedSummaryToNotes.set(folder, list);
|
|
584
|
+
if (!linked)
|
|
585
|
+
noteHasUnlinked = true;
|
|
586
|
+
}
|
|
587
|
+
if (noteHasUnlinked)
|
|
588
|
+
notesWithUnlinkedCitations.add(noteRel);
|
|
589
|
+
if (noteHasAmbiguous)
|
|
590
|
+
notesWithAmbiguousCitations.add(noteRel);
|
|
591
|
+
}
|
|
592
|
+
const citedSummaries = [...citedSummaryToNotes.keys()];
|
|
593
|
+
const orphanedSummaries = citedSummaries
|
|
594
|
+
.filter((summary) => !linkedSummaries.has(summary))
|
|
595
|
+
.sort((left, right) => left.localeCompare(right));
|
|
596
|
+
return {
|
|
597
|
+
citedSummaries,
|
|
598
|
+
linkedSummaries,
|
|
599
|
+
orphanedSummaries,
|
|
600
|
+
ambiguousCitations: [...notesWithAmbiguousCitations].sort((left, right) => left.localeCompare(right)),
|
|
601
|
+
notesWithUnlinkedCitations: [...notesWithUnlinkedCitations].sort((left, right) => left.localeCompare(right)),
|
|
602
|
+
notesScanned,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
function checkPhrases(check) {
|
|
606
|
+
if (Array.isArray(check.params?.phrases)) {
|
|
607
|
+
return check.params.phrases.filter((phrase) => typeof phrase === "string");
|
|
608
|
+
}
|
|
609
|
+
if (typeof check.params?.text === "string") {
|
|
610
|
+
return [check.params.text];
|
|
611
|
+
}
|
|
612
|
+
return [];
|
|
613
|
+
}
|
|
614
|
+
function frontmatterKeys(check) {
|
|
615
|
+
return Array.isArray(check.params?.keys)
|
|
616
|
+
? check.params.keys.filter((key) => typeof key === "string" && key.trim().length > 0)
|
|
617
|
+
: [];
|
|
618
|
+
}
|
|
619
|
+
function hasNonEmptyFrontmatterValue(value) {
|
|
620
|
+
if (typeof value === "string")
|
|
621
|
+
return value.trim().length > 0;
|
|
622
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
623
|
+
return true;
|
|
624
|
+
if (Array.isArray(value))
|
|
625
|
+
return value.some(hasNonEmptyFrontmatterValue);
|
|
626
|
+
if (value && typeof value === "object")
|
|
627
|
+
return Object.keys(value).length > 0;
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
function collectFrontmatterFailures(files, predicate) {
|
|
631
|
+
const invalid = [];
|
|
632
|
+
const missing = [];
|
|
633
|
+
for (const file of files) {
|
|
634
|
+
const parsed = parseJsonFrontmatter(readFileSync(file, "utf8"));
|
|
635
|
+
if (!parsed) {
|
|
636
|
+
invalid.push(file);
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
if (!predicate(parsed.frontmatter)) {
|
|
640
|
+
missing.push(file);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return { invalid, missing };
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Declarative exemption clause for `source_refs_required`: `params.exempt_when`
|
|
647
|
+
* maps a frontmatter key to the values that exempt a note from the requirement
|
|
648
|
+
* (an empty value list means "any non-empty value exempts"). A note is exempt
|
|
649
|
+
* when it declares any matching signal — e.g. a pure index/navigation note that
|
|
650
|
+
* asserts nothing can opt out with `note_role: index` rather than being pushed
|
|
651
|
+
* to fabricate `source_refs`. This is config, not engine taxonomy: the keys and
|
|
652
|
+
* values live in the Build Plan, so no concept is hardcoded in the runtime.
|
|
653
|
+
*/
|
|
654
|
+
function exemptWhenClause(check) {
|
|
655
|
+
const raw = check.params?.exempt_when;
|
|
656
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
657
|
+
return {};
|
|
658
|
+
const out = {};
|
|
659
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
660
|
+
if (typeof key !== "string" || key.trim().length === 0)
|
|
661
|
+
continue;
|
|
662
|
+
out[key] = Array.isArray(value)
|
|
663
|
+
? value.filter((entry) => typeof entry === "string").map((entry) => entry.trim().toLowerCase())
|
|
664
|
+
: [];
|
|
665
|
+
}
|
|
666
|
+
return out;
|
|
667
|
+
}
|
|
668
|
+
function isExemptFromSourceRefs(frontmatter, exemptWhen) {
|
|
669
|
+
for (const [key, allowed] of Object.entries(exemptWhen)) {
|
|
670
|
+
const value = frontmatter[key];
|
|
671
|
+
if (!hasNonEmptyFrontmatterValue(value))
|
|
672
|
+
continue;
|
|
673
|
+
if (allowed.length === 0)
|
|
674
|
+
return true;
|
|
675
|
+
if (typeof value === "string" && allowed.includes(value.trim().toLowerCase()))
|
|
676
|
+
return true;
|
|
677
|
+
if (Array.isArray(value) && value.some((entry) => typeof entry === "string" && allowed.includes(entry.trim().toLowerCase()))) {
|
|
678
|
+
return true;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return false;
|
|
682
|
+
}
|
|
683
|
+
function loadSourceManifestForCheck(context, path = ".interf/runtime/source-manifest.json") {
|
|
684
|
+
const manifestPath = resolve(context.rootPath, path);
|
|
685
|
+
if (!existsSync(manifestPath))
|
|
686
|
+
return { manifest: null, error: `Source Manifest does not exist at ${path}.` };
|
|
687
|
+
try {
|
|
688
|
+
const manifest = SourceManifestSchema.parse(JSON.parse(readFileSync(manifestPath, "utf8")));
|
|
689
|
+
return { manifest, error: null };
|
|
690
|
+
}
|
|
691
|
+
catch (error) {
|
|
692
|
+
return {
|
|
693
|
+
manifest: null,
|
|
694
|
+
error: `Source Manifest is invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
function loadRequiredSourceManifestForCheck(check, context) {
|
|
699
|
+
const manifestPath = typeof check.params?.manifest_path === "string"
|
|
700
|
+
? check.params.manifest_path
|
|
701
|
+
: ".interf/runtime/source-manifest.json";
|
|
702
|
+
const loaded = loadSourceManifestForCheck(context, manifestPath);
|
|
703
|
+
if (loaded.manifest)
|
|
704
|
+
return { manifest: loaded.manifest, failure: null };
|
|
705
|
+
return {
|
|
706
|
+
manifest: null,
|
|
707
|
+
failure: makeCheckResult(check, false, loaded.error ?? "Source Manifest is required for this source-bound check.", { manifest_path: manifestPath }),
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
function sourceManifestPageTotal(context) {
|
|
711
|
+
const loaded = loadSourceManifestForCheck(context);
|
|
712
|
+
if (!loaded.manifest)
|
|
713
|
+
return { total: 0, error: loaded.error };
|
|
714
|
+
let total = 0;
|
|
715
|
+
for (const file of loaded.manifest.files) {
|
|
716
|
+
const pageUnits = file.inspectable_units.filter((unit) => unit.kind === "page").length;
|
|
717
|
+
total += pageUnits > 0 ? pageUnits : file.page_count ?? 0;
|
|
718
|
+
}
|
|
719
|
+
return { total, error: null };
|
|
720
|
+
}
|
|
721
|
+
const EVALUATORS = {
|
|
722
|
+
file_exists(check, context) {
|
|
723
|
+
const target = resolveTargetPath(check, context);
|
|
724
|
+
if (!target) {
|
|
725
|
+
return makeCheckResult(check, false, "No target path provided for file_exists check.");
|
|
726
|
+
}
|
|
727
|
+
if (!existsSync(target)) {
|
|
728
|
+
return makeCheckResult(check, false, "File or directory does not exist.", { path: context.targetPath });
|
|
729
|
+
}
|
|
730
|
+
try {
|
|
731
|
+
const stats = statSync(target);
|
|
732
|
+
if (stats.isFile() && stats.size === 0) {
|
|
733
|
+
return makeCheckResult(check, false, "File exists but is empty.", { path: context.targetPath });
|
|
734
|
+
}
|
|
735
|
+
return makeCheckResult(check, true, `Path exists (${stats.isDirectory() ? "directory" : "file"}).`);
|
|
736
|
+
}
|
|
737
|
+
catch {
|
|
738
|
+
return makeCheckResult(check, false, "Could not stat target path.", { path: context.targetPath });
|
|
739
|
+
}
|
|
740
|
+
},
|
|
741
|
+
source_manifest_valid(check, context) {
|
|
742
|
+
const manifestPath = typeof check.params?.path === "string"
|
|
743
|
+
? check.params.path
|
|
744
|
+
: ".interf/runtime/source-manifest.json";
|
|
745
|
+
const loaded = loadSourceManifestForCheck(context, manifestPath);
|
|
746
|
+
if (!loaded.manifest) {
|
|
747
|
+
return makeCheckResult(check, false, loaded.error ?? "Source Manifest is invalid.");
|
|
748
|
+
}
|
|
749
|
+
return makeCheckResult(check, true, `Source Manifest lists ${loaded.manifest.source_total} source file(s).`, { source_manifest_id: loaded.manifest.manifest_id, source_total: loaded.manifest.source_total });
|
|
750
|
+
},
|
|
751
|
+
min_file_count(check, context) {
|
|
752
|
+
const target = resolveTargetPath(check, context);
|
|
753
|
+
if (!target) {
|
|
754
|
+
return makeCheckResult(check, false, "No target path provided for min_file_count check.");
|
|
755
|
+
}
|
|
756
|
+
const min = typeof check.params?.min === "number" ? check.params.min : 1;
|
|
757
|
+
const actual = countFiles(target);
|
|
758
|
+
if (actual >= min) {
|
|
759
|
+
return makeCheckResult(check, true, `${actual} file(s) (≥ ${min}).`, { actual, min });
|
|
760
|
+
}
|
|
761
|
+
return makeCheckResult(check, false, `Found ${actual} file(s); expected at least ${min}.`, { actual, min });
|
|
762
|
+
},
|
|
763
|
+
min_file_count_matches_source(check, context) {
|
|
764
|
+
const target = resolveTargetPath(check, context);
|
|
765
|
+
if (!target) {
|
|
766
|
+
return makeCheckResult(check, false, "No target path provided for min_file_count_matches_source check.");
|
|
767
|
+
}
|
|
768
|
+
const sourceManifest = loadRequiredSourceManifestForCheck(check, context);
|
|
769
|
+
if (!sourceManifest.manifest)
|
|
770
|
+
return sourceManifest.failure ?? makeCheckResult(check, false, "Source Manifest is required.");
|
|
771
|
+
const matchKey = typeof check.params?.match === "string" ? check.params.match : "source_total";
|
|
772
|
+
const expected = matchKey === "source_total"
|
|
773
|
+
? sourceManifest.manifest.source_total
|
|
774
|
+
: context.counts?.[matchKey];
|
|
775
|
+
if (typeof expected !== "number") {
|
|
776
|
+
return makeCheckResult(check, false, `Cannot evaluate: count "${matchKey}" not available in context.`, { matchKey });
|
|
777
|
+
}
|
|
778
|
+
if (expected <= 0) {
|
|
779
|
+
return makeCheckResult(check, false, `Cannot evaluate: count "${matchKey}" must be greater than zero.`, { matchKey, expected });
|
|
780
|
+
}
|
|
781
|
+
const actual = countFiles(target);
|
|
782
|
+
if (actual >= expected) {
|
|
783
|
+
return makeCheckResult(check, true, `${actual} of ${expected} source files covered.`, { actual, expected, source_manifest_id: sourceManifest.manifest.manifest_id });
|
|
784
|
+
}
|
|
785
|
+
return makeCheckResult(check, false, `Only ${actual} of ${expected} source files covered.`, { actual, expected, matchKey, source_manifest_id: sourceManifest.manifest.manifest_id });
|
|
786
|
+
},
|
|
787
|
+
source_summary_folders(check, context) {
|
|
788
|
+
const target = resolveTargetPath(check, context);
|
|
789
|
+
if (!target) {
|
|
790
|
+
return makeCheckResult(check, false, "No target path provided for source_summary_folders check.");
|
|
791
|
+
}
|
|
792
|
+
const sourceManifest = loadRequiredSourceManifestForCheck(check, context);
|
|
793
|
+
if (!sourceManifest.manifest)
|
|
794
|
+
return sourceManifest.failure ?? makeCheckResult(check, false, "Source Manifest is required.");
|
|
795
|
+
const matchKey = typeof check.params?.match === "string" ? check.params.match : "source_total";
|
|
796
|
+
const expected = matchKey === "source_total"
|
|
797
|
+
? sourceManifest.manifest.source_total
|
|
798
|
+
: context.counts?.[matchKey];
|
|
799
|
+
if (typeof expected !== "number") {
|
|
800
|
+
return makeCheckResult(check, false, `Cannot evaluate: count "${matchKey}" not available in context.`, { matchKey });
|
|
801
|
+
}
|
|
802
|
+
if (expected <= 0) {
|
|
803
|
+
return makeCheckResult(check, false, `Cannot evaluate: count "${matchKey}" must be greater than zero.`, { matchKey, expected });
|
|
804
|
+
}
|
|
805
|
+
const summaryFile = typeof check.params?.summary_file === "string" ? check.params.summary_file : "summary.md";
|
|
806
|
+
const manifestFile = typeof check.params?.manifest_file === "string" ? check.params.manifest_file : "manifest.md";
|
|
807
|
+
const coverage = matchKey === "source_total"
|
|
808
|
+
? sourceSummaryCoverage(target, sourceManifest.manifest, { summaryFile, manifestFile })
|
|
809
|
+
: {
|
|
810
|
+
...countSourceSummaryFolders(target, { summaryFile, manifestFile }),
|
|
811
|
+
expected,
|
|
812
|
+
missingSourcePaths: [],
|
|
813
|
+
extraSummaryFolders: [],
|
|
814
|
+
};
|
|
815
|
+
if (coverage.actual >= expected) {
|
|
816
|
+
return makeCheckResult(check, true, `${coverage.actual} source summary folder(s) covered for ${expected} source file(s).`, {
|
|
817
|
+
actual: coverage.actual,
|
|
818
|
+
expected,
|
|
819
|
+
summaryFile,
|
|
820
|
+
manifestFile,
|
|
821
|
+
source_manifest_id: sourceManifest.manifest.manifest_id,
|
|
822
|
+
extraSummaryFolders: coverage.extraSummaryFolders,
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
return makeCheckResult(check, false, `Only ${coverage.actual} source summary folder(s) covered for ${expected} source file(s).`, {
|
|
826
|
+
actual: coverage.actual,
|
|
827
|
+
expected,
|
|
828
|
+
matchKey,
|
|
829
|
+
summaryFile,
|
|
830
|
+
manifestFile,
|
|
831
|
+
missingSourcePaths: coverage.missingSourcePaths,
|
|
832
|
+
extraSummaryFolders: coverage.extraSummaryFolders,
|
|
833
|
+
});
|
|
834
|
+
},
|
|
835
|
+
source_page_coverage(check, context) {
|
|
836
|
+
const target = resolveTargetPath(check, context);
|
|
837
|
+
if (!target) {
|
|
838
|
+
return makeCheckResult(check, false, "No target path provided for source_page_coverage check.");
|
|
839
|
+
}
|
|
840
|
+
const expected = sourceManifestPageTotal(context);
|
|
841
|
+
if (expected.error)
|
|
842
|
+
return makeCheckResult(check, false, expected.error);
|
|
843
|
+
if (expected.total <= 0) {
|
|
844
|
+
if (check.params?.allow_no_pages === true) {
|
|
845
|
+
return makeCheckResult(check, true, "Source Manifest does not declare page-level units.");
|
|
846
|
+
}
|
|
847
|
+
return makeCheckResult(check, false, "Source Manifest does not declare page-level units. Add page_count or page inspectable_units, or set params.allow_no_pages explicitly.");
|
|
848
|
+
}
|
|
849
|
+
const pagesDir = typeof check.params?.pages_dir === "string" ? check.params.pages_dir : "pages";
|
|
850
|
+
const summaryFile = typeof check.params?.summary_file === "string" ? check.params.summary_file : "summary.md";
|
|
851
|
+
const actual = listFilesRecursive(target)
|
|
852
|
+
.filter((file) => file.endsWith(`/${summaryFile}`) && file.includes(`/${pagesDir}/`))
|
|
853
|
+
.length;
|
|
854
|
+
if (actual >= expected.total) {
|
|
855
|
+
return makeCheckResult(check, true, `${actual} page summary file(s) cover ${expected.total} page unit(s).`, { actual, expected: expected.total });
|
|
856
|
+
}
|
|
857
|
+
return makeCheckResult(check, false, `Only ${actual} page summary file(s) cover ${expected.total} page unit(s).`, { actual, expected: expected.total, pagesDir, summaryFile });
|
|
858
|
+
},
|
|
859
|
+
frontmatter_valid(check, context) {
|
|
860
|
+
const target = resolveTargetPath(check, context);
|
|
861
|
+
if (!target) {
|
|
862
|
+
return makeCheckResult(check, false, "No target path provided for frontmatter_valid check.");
|
|
863
|
+
}
|
|
864
|
+
const files = listMarkdownFiles(target);
|
|
865
|
+
if (files.length === 0) {
|
|
866
|
+
return makeCheckResult(check, true, "No markdown files to validate.");
|
|
867
|
+
}
|
|
868
|
+
const validation = validateSynthFiles(files);
|
|
869
|
+
if (validation.invalid_frontmatter === 0) {
|
|
870
|
+
return makeCheckResult(check, true, `${files.length} markdown file(s) have valid frontmatter.`);
|
|
871
|
+
}
|
|
872
|
+
return makeCheckResult(check, false, `${validation.invalid_frontmatter} of ${files.length} markdown file(s) have invalid frontmatter.`, { invalid: validation.invalid_frontmatter, total: files.length });
|
|
873
|
+
},
|
|
874
|
+
frontmatter_required_keys(check, context) {
|
|
875
|
+
const target = resolveTargetPath(check, context);
|
|
876
|
+
if (!target) {
|
|
877
|
+
return makeCheckResult(check, false, "No target path provided for frontmatter_required_keys check.");
|
|
878
|
+
}
|
|
879
|
+
const keys = frontmatterKeys(check);
|
|
880
|
+
if (keys.length === 0) {
|
|
881
|
+
return makeCheckResult(check, false, "Build Plan check is missing required frontmatter keys. Use `params.keys: string[]`.");
|
|
882
|
+
}
|
|
883
|
+
const files = listMarkdownFiles(target);
|
|
884
|
+
if (files.length === 0) {
|
|
885
|
+
return makeCheckResult(check, true, "No markdown files to validate.");
|
|
886
|
+
}
|
|
887
|
+
const validation = validateSynthFiles(files, { requiredFrontmatterKeys: keys });
|
|
888
|
+
if (validation.invalid_frontmatter === 0) {
|
|
889
|
+
return makeCheckResult(check, true, `All ${files.length} markdown file(s) have required keys: ${keys.join(", ")}.`);
|
|
890
|
+
}
|
|
891
|
+
return makeCheckResult(check, false, `${validation.invalid_frontmatter} of ${files.length} file(s) missing required keys (${keys.join(", ")}).`, { invalid: validation.invalid_frontmatter, total: files.length, requiredKeys: keys });
|
|
892
|
+
},
|
|
893
|
+
frontmatter_nonempty_keys(check, context) {
|
|
894
|
+
const target = resolveTargetPath(check, context);
|
|
895
|
+
if (!target) {
|
|
896
|
+
return makeCheckResult(check, false, "No target path provided for frontmatter_nonempty_keys check.");
|
|
897
|
+
}
|
|
898
|
+
const keys = frontmatterKeys(check);
|
|
899
|
+
if (keys.length === 0) {
|
|
900
|
+
return makeCheckResult(check, false, "Build Plan check is missing non-empty frontmatter keys. Use `params.keys: string[]`.");
|
|
901
|
+
}
|
|
902
|
+
const files = listMarkdownFiles(target);
|
|
903
|
+
if (files.length === 0) {
|
|
904
|
+
return makeCheckResult(check, false, "No markdown files to validate.");
|
|
905
|
+
}
|
|
906
|
+
const { invalid, missing } = collectFrontmatterFailures(files, (frontmatter) => keys.every((key) => hasNonEmptyFrontmatterValue(frontmatter[key])));
|
|
907
|
+
if (invalid.length === 0 && missing.length === 0) {
|
|
908
|
+
return makeCheckResult(check, true, `All ${files.length} markdown file(s) have non-empty keys: ${keys.join(", ")}.`);
|
|
909
|
+
}
|
|
910
|
+
return makeCheckResult(check, false, `${invalid.length + missing.length} of ${files.length} markdown file(s) are missing non-empty keys (${keys.join(", ")}).`, { invalid, missing, requiredKeys: keys });
|
|
911
|
+
},
|
|
912
|
+
source_refs_required(check, context) {
|
|
913
|
+
const target = resolveTargetPath(check, context);
|
|
914
|
+
if (!target) {
|
|
915
|
+
return makeCheckResult(check, false, "No target path provided for source_refs_required check.");
|
|
916
|
+
}
|
|
917
|
+
const keys = frontmatterKeys(check);
|
|
918
|
+
const sourceRefKeys = keys.length > 0 ? keys : ["source_refs", "source_ref", "source_path"];
|
|
919
|
+
const exemptWhen = exemptWhenClause(check);
|
|
920
|
+
const files = listMarkdownFiles(target);
|
|
921
|
+
if (files.length === 0) {
|
|
922
|
+
return makeCheckResult(check, false, "No markdown files to validate.");
|
|
923
|
+
}
|
|
924
|
+
const { invalid, missing } = collectFrontmatterFailures(files, (frontmatter) => isExemptFromSourceRefs(frontmatter, exemptWhen)
|
|
925
|
+
|| sourceRefKeys.some((key) => hasNonEmptyFrontmatterValue(frontmatter[key])));
|
|
926
|
+
if (invalid.length === 0 && missing.length === 0) {
|
|
927
|
+
return makeCheckResult(check, true, `All ${files.length} markdown file(s) have source refs.`);
|
|
928
|
+
}
|
|
929
|
+
return makeCheckResult(check, false, `${invalid.length + missing.length} of ${files.length} markdown file(s) are missing source refs.`, { invalid, missing, sourceRefKeys });
|
|
930
|
+
},
|
|
931
|
+
summary_backlinks_present(check, context) {
|
|
932
|
+
const target = resolveTargetPath(check, context);
|
|
933
|
+
if (!target) {
|
|
934
|
+
return makeCheckResult(check, false, "No target path provided for summary_backlinks_present check.");
|
|
935
|
+
}
|
|
936
|
+
const summariesDir = typeof check.params?.summaries_dir === "string" ? check.params.summaries_dir : "summaries";
|
|
937
|
+
const summaryFile = typeof check.params?.summary_file === "string" ? check.params.summary_file : "summary.md";
|
|
938
|
+
const manifestFile = typeof check.params?.manifest_file === "string" ? check.params.manifest_file : "manifest.md";
|
|
939
|
+
const analysis = analyzeSummaryBacklinks(target, context, { summariesDir, summaryFile, manifestFile });
|
|
940
|
+
// An ambiguous basename-only citation cannot be proven backlinked: it names a
|
|
941
|
+
// basename two or more summary folders share, so we refuse to credit one
|
|
942
|
+
// arbitrarily. Surface it as a gap rather than silently passing — that is the
|
|
943
|
+
// exact orphan the old first-basename-wins alias hid. Fails even when nothing
|
|
944
|
+
// resolved cleanly, because the citation itself is unverifiable.
|
|
945
|
+
if (analysis.ambiguousCitations.length > 0) {
|
|
946
|
+
return makeCheckResult(check, false, `${analysis.ambiguousCitations.length} note(s) cite a summary by an ambiguous basename that two or more summary folders share; cite the full summary-folder path so the backlink can be verified.`, {
|
|
947
|
+
cited_summaries: analysis.citedSummaries.length,
|
|
948
|
+
linked_summaries: analysis.linkedSummaries.size,
|
|
949
|
+
ambiguous_citations: analysis.ambiguousCitations.length,
|
|
950
|
+
notes_with_ambiguous_citations: analysis.ambiguousCitations,
|
|
951
|
+
orphaned_summaries: analysis.orphanedSummaries.length,
|
|
952
|
+
orphaned_summary_folders: analysis.orphanedSummaries,
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
if (analysis.citedSummaries.length === 0) {
|
|
956
|
+
return makeCheckResult(check, true, "No notes cite a summary source ref yet; nothing to backlink.", { notesScanned: analysis.notesScanned });
|
|
957
|
+
}
|
|
958
|
+
if (analysis.orphanedSummaries.length === 0) {
|
|
959
|
+
return makeCheckResult(check, true, `All ${analysis.citedSummaries.length} cited summary folder(s) are wikilinked from this layer.`, {
|
|
960
|
+
cited_summaries: analysis.citedSummaries.length,
|
|
961
|
+
linked_summaries: analysis.linkedSummaries.size,
|
|
962
|
+
orphaned_summaries: 0,
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
return makeCheckResult(check, false, `${analysis.orphanedSummaries.length} of ${analysis.citedSummaries.length} cited summary folder(s) are orphaned (cited via source_refs but never wikilinked).`, {
|
|
966
|
+
cited_summaries: analysis.citedSummaries.length,
|
|
967
|
+
linked_summaries: analysis.linkedSummaries.size,
|
|
968
|
+
orphaned_summaries: analysis.orphanedSummaries.length,
|
|
969
|
+
orphaned_summary_folders: analysis.orphanedSummaries,
|
|
970
|
+
notes_with_unlinked_citations: analysis.notesWithUnlinkedCitations,
|
|
971
|
+
});
|
|
972
|
+
},
|
|
973
|
+
knowledge_web_connectivity(check, context) {
|
|
974
|
+
const target = resolveTargetPath(check, context);
|
|
975
|
+
if (!target) {
|
|
976
|
+
return makeCheckResult(check, false, "No target path provided for knowledge_web_connectivity check.");
|
|
977
|
+
}
|
|
978
|
+
// The artifact's target path IS the knowledge layer to scan — derived from the
|
|
979
|
+
// artifact this check is attached to, never hardcoded to `knowledge/` from the
|
|
980
|
+
// root. `params.knowledge_dir` is a graph-root-relative override that may only
|
|
981
|
+
// NARROW the scan to a sub-directory inside the target; an override that escapes
|
|
982
|
+
// the target layer (e.g. a sibling layer) is rejected so the check cannot be
|
|
983
|
+
// pointed at a different, possibly-passing subtree.
|
|
984
|
+
let scanRoot = target;
|
|
985
|
+
if (typeof check.params?.knowledge_dir === "string" && check.params.knowledge_dir.trim().length > 0) {
|
|
986
|
+
const narrowed = resolve(context.rootPath, check.params.knowledge_dir);
|
|
987
|
+
if (narrowed !== target && !narrowed.startsWith(`${target}/`)) {
|
|
988
|
+
return makeCheckResult(check, false, "params.knowledge_dir resolves outside this check's target layer; it may only narrow the scan inside the target.", { knowledge_dir: check.params.knowledge_dir, target_dir: context.targetPath });
|
|
989
|
+
}
|
|
990
|
+
scanRoot = narrowed;
|
|
991
|
+
}
|
|
992
|
+
const web = analyzeNoteWeb(listMarkdownFiles(scanRoot), context);
|
|
993
|
+
// A bare basename two or more notes share cannot be proven to connect either
|
|
994
|
+
// namesake, so it credits no edge — surface it as a "cite the full path" gap
|
|
995
|
+
// rather than silently connecting the linker and hiding an island. Fails even
|
|
996
|
+
// when no island remains, because the link itself is unverifiable.
|
|
997
|
+
if (web.ambiguousLinkNotes.length > 0) {
|
|
998
|
+
return makeCheckResult(check, false, `${web.ambiguousLinkNotes.length} knowledge note(s) link another knowledge note by an ambiguous basename two or more notes share; link the full graph path so the web edge can be verified.`, {
|
|
999
|
+
knowledge_notes: web.notes,
|
|
1000
|
+
connected: web.connected,
|
|
1001
|
+
islands: web.islands.length,
|
|
1002
|
+
island_notes: web.islands,
|
|
1003
|
+
ambiguous_links: web.ambiguousLinkNotes.length,
|
|
1004
|
+
notes_with_ambiguous_links: web.ambiguousLinkNotes,
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
if (web.islands.length === 0) {
|
|
1008
|
+
return makeCheckResult(check, true, web.notes <= 1
|
|
1009
|
+
? `${web.notes} knowledge note(s); too few to form a web.`
|
|
1010
|
+
: `${web.connected} / ${web.notes} knowledge notes link another knowledge note.`, { knowledge_notes: web.notes, connected: web.connected, islands: 0 });
|
|
1011
|
+
}
|
|
1012
|
+
return makeCheckResult(check, false, `${web.islands.length} of ${web.notes} knowledge note(s) link no other knowledge note (disconnected island${web.islands.length === 1 ? "" : "s"}).`, {
|
|
1013
|
+
knowledge_notes: web.notes,
|
|
1014
|
+
connected: web.connected,
|
|
1015
|
+
islands: web.islands.length,
|
|
1016
|
+
island_notes: web.islands,
|
|
1017
|
+
});
|
|
1018
|
+
},
|
|
1019
|
+
/**
|
|
1020
|
+
* Whole-graph connectivity floor. The `knowledge_web_connectivity` check only
|
|
1021
|
+
* scans the `knowledge/` layer, and `summary_backlinks_present` only flags a
|
|
1022
|
+
* summary that a knowledge note CITES via source_refs but does not wikilink.
|
|
1023
|
+
* Neither sees a summary that no note cites at all — so a Context Graph can
|
|
1024
|
+
* pass readiness while the bulk of its `summaries/` notes are free-floating
|
|
1025
|
+
* islands no entrypoint or note reaches. This is the "no disconnected island
|
|
1026
|
+
* passing silently" rule applied to the WHOLE graph, not just the 7 knowledge
|
|
1027
|
+
* notes: every markdown note across `summaries/`, `knowledge/`, `artifacts/`,
|
|
1028
|
+
* and `home.md` must have undirected degree ≥ 1 in the note-link web. A note no
|
|
1029
|
+
* other note links AND that links no other note is a disconnected island and
|
|
1030
|
+
* fails readiness. Connectedness, not a count — one genuine inbound or outbound
|
|
1031
|
+
* edge is enough. By default the scan root is the Context Graph root; a Build
|
|
1032
|
+
* Plan may pass `params.graph_root` to narrow the floor to one layer (it may
|
|
1033
|
+
* only narrow inside the graph root, never escape it).
|
|
1034
|
+
*/
|
|
1035
|
+
graph_notes_connected(check, context) {
|
|
1036
|
+
// Default scope: the canonical CONTENT layers of the whole Context Graph
|
|
1037
|
+
// (summaries/, knowledge/, artifacts/, home.md) as one web — NOT the raw graph
|
|
1038
|
+
// root, which also holds runtime scaffolding (.interf/, .claude/, SKILL.md
|
|
1039
|
+
// docs, CLAUDE.md) that is not graph content and would inject false islands.
|
|
1040
|
+
// Intentionally spans all CONTENT layers, not `targetPath`: a knowledge note
|
|
1041
|
+
// or an entrypoint route may be the only thing connecting a summary, so scoping
|
|
1042
|
+
// to one layer in isolation would re-introduce the uncited-summary-island bug.
|
|
1043
|
+
const graphRoot = resolve(context.rootPath);
|
|
1044
|
+
let noteFiles;
|
|
1045
|
+
if (typeof check.params?.graph_root === "string" && check.params.graph_root.trim().length > 0) {
|
|
1046
|
+
const narrowed = resolve(graphRoot, check.params.graph_root);
|
|
1047
|
+
// May only NARROW inside the graph root; an override that escapes the root
|
|
1048
|
+
// is rejected so the floor cannot be pointed at a different, passing tree.
|
|
1049
|
+
if (narrowed !== graphRoot && !narrowed.startsWith(`${graphRoot}/`)) {
|
|
1050
|
+
return makeCheckResult(check, false, "params.graph_root resolves outside the Context Graph root; it may only narrow the connectivity floor to a path inside the graph.", { graph_root: check.params.graph_root });
|
|
1051
|
+
}
|
|
1052
|
+
// A narrowed scope scans that subtree's markdown notes directly (the caller
|
|
1053
|
+
// is naming a content path, so no scaffolding filter is applied beyond the
|
|
1054
|
+
// shared output-markdown filter).
|
|
1055
|
+
noteFiles = listMarkdownFiles(narrowed);
|
|
1056
|
+
}
|
|
1057
|
+
else {
|
|
1058
|
+
noteFiles = collectGraphContentNotes(graphRoot);
|
|
1059
|
+
}
|
|
1060
|
+
const web = analyzeNoteWeb(noteFiles, context);
|
|
1061
|
+
if (web.ambiguousLinkNotes.length > 0) {
|
|
1062
|
+
return makeCheckResult(check, false, `${web.ambiguousLinkNotes.length} note(s) link another note by an ambiguous basename two or more notes share; link the full graph path so the web edge can be verified.`, {
|
|
1063
|
+
graph_notes: web.notes,
|
|
1064
|
+
connected: web.connected,
|
|
1065
|
+
islands: web.islands.length,
|
|
1066
|
+
island_notes: web.islands,
|
|
1067
|
+
ambiguous_links: web.ambiguousLinkNotes.length,
|
|
1068
|
+
notes_with_ambiguous_links: web.ambiguousLinkNotes,
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
if (web.islands.length === 0) {
|
|
1072
|
+
return makeCheckResult(check, true, web.notes <= 1
|
|
1073
|
+
? `${web.notes} note(s); too few to form a web.`
|
|
1074
|
+
: `${web.connected} / ${web.notes} notes are link-connected to another note (no islands).`, { graph_notes: web.notes, connected: web.connected, islands: 0 });
|
|
1075
|
+
}
|
|
1076
|
+
return makeCheckResult(check, false, `${web.islands.length} of ${web.notes} note(s) link no other note and are linked by none (disconnected island${web.islands.length === 1 ? "" : "s"}). Every Context Graph note — including every summary — must be reachable through the link web.`, {
|
|
1077
|
+
graph_notes: web.notes,
|
|
1078
|
+
connected: web.connected,
|
|
1079
|
+
islands: web.islands.length,
|
|
1080
|
+
island_notes: web.islands,
|
|
1081
|
+
});
|
|
1082
|
+
},
|
|
1083
|
+
wikilinks_valid(check, context) {
|
|
1084
|
+
const target = resolveTargetPath(check, context);
|
|
1085
|
+
if (!target) {
|
|
1086
|
+
return makeCheckResult(check, false, "No target path provided for wikilinks_valid check.");
|
|
1087
|
+
}
|
|
1088
|
+
const broken = countBrokenWikilinks(context.rootPath, [context.rootPath], [target]);
|
|
1089
|
+
if (broken === 0) {
|
|
1090
|
+
return makeCheckResult(check, true, "All wikilinks resolve.");
|
|
1091
|
+
}
|
|
1092
|
+
return makeCheckResult(check, false, `${broken} broken wikilink(s).`, { broken });
|
|
1093
|
+
},
|
|
1094
|
+
must_not_contain(check, context) {
|
|
1095
|
+
const target = resolveTargetPath(check, context);
|
|
1096
|
+
if (!target || !existsSync(target)) {
|
|
1097
|
+
return makeCheckResult(check, false, "Target path does not exist.", { path: context.targetPath });
|
|
1098
|
+
}
|
|
1099
|
+
const phrases = checkPhrases(check);
|
|
1100
|
+
if (phrases.length === 0) {
|
|
1101
|
+
return makeCheckResult(check, false, "Requested output diagnostic is missing forbidden text. Use `params.phrases: string[]` or `params.text: string`.");
|
|
1102
|
+
}
|
|
1103
|
+
try {
|
|
1104
|
+
const stats = statSync(target);
|
|
1105
|
+
const files = stats.isFile() ? [target] : listFilesRecursive(target, () => true);
|
|
1106
|
+
const offenders = [];
|
|
1107
|
+
for (const file of files) {
|
|
1108
|
+
const content = readFileSync(file, "utf8");
|
|
1109
|
+
for (const phrase of phrases) {
|
|
1110
|
+
if (content.includes(phrase)) {
|
|
1111
|
+
offenders.push(`${file}: contains "${phrase}"`);
|
|
1112
|
+
break;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
if (offenders.length === 0) {
|
|
1117
|
+
return makeCheckResult(check, true, `No forbidden phrases found across ${files.length} file(s).`);
|
|
1118
|
+
}
|
|
1119
|
+
return makeCheckResult(check, false, `${offenders.length} file(s) contain forbidden phrases.`, { offenders, phrases });
|
|
1120
|
+
}
|
|
1121
|
+
catch (error) {
|
|
1122
|
+
return makeCheckResult(check, false, `Error reading target: ${error instanceof Error ? error.message : String(error)}`);
|
|
1123
|
+
}
|
|
1124
|
+
},
|
|
1125
|
+
must_contain(check, context) {
|
|
1126
|
+
const target = resolveTargetPath(check, context);
|
|
1127
|
+
if (!target || !existsSync(target)) {
|
|
1128
|
+
return makeCheckResult(check, false, "Target path does not exist.", { path: context.targetPath });
|
|
1129
|
+
}
|
|
1130
|
+
const phrases = checkPhrases(check);
|
|
1131
|
+
if (phrases.length === 0) {
|
|
1132
|
+
return makeCheckResult(check, false, "Requested output diagnostic is missing required text. Use `params.phrases: string[]` or `params.text: string`.");
|
|
1133
|
+
}
|
|
1134
|
+
try {
|
|
1135
|
+
const stats = statSync(target);
|
|
1136
|
+
// For directories, must_contain checks across ALL files (every phrase
|
|
1137
|
+
// must appear in at least one file). For files, all phrases must
|
|
1138
|
+
// appear in that single file.
|
|
1139
|
+
if (stats.isFile()) {
|
|
1140
|
+
const content = readFileSync(target, "utf8");
|
|
1141
|
+
const missing = phrases.filter((phrase) => !content.includes(phrase));
|
|
1142
|
+
if (missing.length === 0) {
|
|
1143
|
+
return makeCheckResult(check, true, `All ${phrases.length} required phrase(s) present.`);
|
|
1144
|
+
}
|
|
1145
|
+
return makeCheckResult(check, false, `${missing.length} required phrase(s) missing.`, { missing });
|
|
1146
|
+
}
|
|
1147
|
+
const files = listFilesRecursive(target, () => true);
|
|
1148
|
+
const remaining = new Set(phrases);
|
|
1149
|
+
for (const file of files) {
|
|
1150
|
+
const content = readFileSync(file, "utf8");
|
|
1151
|
+
for (const phrase of [...remaining]) {
|
|
1152
|
+
if (content.includes(phrase))
|
|
1153
|
+
remaining.delete(phrase);
|
|
1154
|
+
}
|
|
1155
|
+
if (remaining.size === 0)
|
|
1156
|
+
break;
|
|
1157
|
+
}
|
|
1158
|
+
if (remaining.size === 0) {
|
|
1159
|
+
return makeCheckResult(check, true, `All ${phrases.length} required phrase(s) found across files.`);
|
|
1160
|
+
}
|
|
1161
|
+
return makeCheckResult(check, false, `${remaining.size} required phrase(s) missing across all files.`, { missing: [...remaining] });
|
|
1162
|
+
}
|
|
1163
|
+
catch (error) {
|
|
1164
|
+
return makeCheckResult(check, false, `Error reading target: ${error instanceof Error ? error.message : String(error)}`);
|
|
1165
|
+
}
|
|
1166
|
+
},
|
|
1167
|
+
qa_match(check, context) {
|
|
1168
|
+
const expected = typeof check.params?.expected === "string" ? check.params.expected : "";
|
|
1169
|
+
const strictness = typeof check.params?.strictness === "string" ? check.params.strictness : "loose";
|
|
1170
|
+
const answer = context.agentAnswer ?? "";
|
|
1171
|
+
if (!expected) {
|
|
1172
|
+
return makeCheckResult(check, false, "qa_match check requires `params.expected: string`.");
|
|
1173
|
+
}
|
|
1174
|
+
if (!answer) {
|
|
1175
|
+
return makeCheckResult(check, false, "No agent answer available for qa_match evaluation.");
|
|
1176
|
+
}
|
|
1177
|
+
if (strictness === "strict") {
|
|
1178
|
+
const passed = answer.trim() === expected.trim();
|
|
1179
|
+
return makeCheckResult(check, passed, passed ? "Answer matches expected exactly." : "Answer does not match expected.");
|
|
1180
|
+
}
|
|
1181
|
+
// Loose: case-insensitive substring match either direction.
|
|
1182
|
+
const a = answer.trim().toLowerCase();
|
|
1183
|
+
const e = expected.trim().toLowerCase();
|
|
1184
|
+
const passed = a.includes(e) || e.includes(a);
|
|
1185
|
+
return makeCheckResult(check, passed, passed ? "Answer matches expected (loose)." : "Answer does not match expected.");
|
|
1186
|
+
},
|
|
1187
|
+
};
|
|
1188
|
+
/**
|
|
1189
|
+
* Evaluate a single Check against a context, returning a CheckResult.
|
|
1190
|
+
*/
|
|
1191
|
+
export function evaluateCheck(check, context) {
|
|
1192
|
+
const evaluator = EVALUATORS[check.kind];
|
|
1193
|
+
if (!evaluator) {
|
|
1194
|
+
return {
|
|
1195
|
+
check_id: check.id,
|
|
1196
|
+
kind: check.kind,
|
|
1197
|
+
passed: false,
|
|
1198
|
+
required: check.required,
|
|
1199
|
+
summary: `Unknown check kind: ${check.kind}`,
|
|
1200
|
+
evaluated_at: new Date().toISOString(),
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
return evaluator(check, context);
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Evaluate a list of checks. Aggregate ready/not_ready verdict over
|
|
1207
|
+
* the check_results: `ready` if every required check passed, otherwise
|
|
1208
|
+
* `not_ready`. Soft checks (required: false) that fail produce a
|
|
1209
|
+
* check result but don't change the verdict.
|
|
1210
|
+
*/
|
|
1211
|
+
export function evaluateChecks(checks, context) {
|
|
1212
|
+
const check_results = checks.map((check) => evaluateCheck(check, context));
|
|
1213
|
+
const failures = check_results.filter((result) => !result.passed && result.required);
|
|
1214
|
+
return {
|
|
1215
|
+
check_results,
|
|
1216
|
+
ready: failures.length === 0,
|
|
1217
|
+
failures,
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Type guard for the canonical CheckKinds. Useful at parse boundaries
|
|
1222
|
+
* where a string came from on-disk JSON.
|
|
1223
|
+
*/
|
|
1224
|
+
export function isCheckKind(value) {
|
|
1225
|
+
return CheckKindSchema.safeParse(value).success;
|
|
1226
|
+
}
|