project-tiny-context-harness 0.2.85 → 0.6.1
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 +164 -427
- package/assets/README.md +284 -455
- package/assets/README.zh-CN.md +200 -123
- package/assets/agents/AGENTS_CORE.md +54 -67
- package/assets/context_templates/context.toml +3 -0
- package/assets/context_templates/global.md +6 -6
- package/assets/github/harness.yml +2 -2
- package/assets/skills/context_development_engineer/SKILL.md +91 -107
- package/assets/skills/context_product_plan/SKILL.md +18 -28
- package/assets/skills/context_surface_contract/SKILL.md +35 -38
- package/assets/skills/context_uiux_design/SKILL.md +31 -30
- package/assets/skills/long-task-workflow/SKILL.md +76 -0
- package/assets/skills/long-task-workflow/agents/openai.yaml +4 -0
- package/assets/skills/long-task-workflow/references/authority-lifecycle.md +41 -0
- package/assets/skills/long-task-workflow/references/contract-authoring.md +63 -0
- package/assets/skills/long-task-workflow/references/evidence-design.md +45 -0
- package/assets/skills/normal-long-task/SKILL.md +12 -489
- package/assets/skills/source-plan-authoring/SKILL.md +467 -0
- package/assets/tools/validate_context.py +293 -127
- package/dist/cli.js +3 -1
- package/dist/commands/check-modularity.js +33 -10
- package/dist/commands/composite-campaign.d.ts +1 -0
- package/dist/commands/composite-campaign.js +8 -0
- package/dist/commands/composite-long-task.d.ts +1 -6
- package/dist/commands/composite-long-task.js +7 -163
- package/dist/commands/delivery-set.d.ts +1 -0
- package/dist/commands/delivery-set.js +8 -0
- package/dist/commands/disable.d.ts +1 -0
- package/dist/commands/disable.js +15 -0
- package/dist/commands/enable.d.ts +1 -0
- package/dist/commands/enable.js +14 -0
- package/dist/commands/export-context-args.js +13 -4
- package/dist/commands/export-context.js +18 -7
- package/dist/commands/index.js +42 -30
- package/dist/commands/init.js +12 -6
- package/dist/commands/long-task-authoring.d.ts +3 -0
- package/dist/commands/long-task-authoring.js +92 -0
- package/dist/commands/long-task-explain.d.ts +1 -0
- package/dist/commands/long-task-explain.js +52 -0
- package/dist/commands/long-task.d.ts +1 -0
- package/dist/commands/long-task.js +226 -0
- package/dist/commands/package-source.js +2 -2
- package/dist/commands/upgrade.js +10 -10
- package/dist/index.d.ts +11 -1
- package/dist/index.js +8 -0
- package/dist/lib/config.js +34 -8
- package/dist/lib/context-default-footprint.d.ts +16 -0
- package/dist/lib/context-default-footprint.js +107 -0
- package/dist/lib/context-export.js +105 -41
- package/dist/lib/context-graph-snapshot.d.ts +18 -0
- package/dist/lib/context-graph-snapshot.js +171 -0
- package/dist/lib/context-manifest-schema.d.ts +28 -0
- package/dist/lib/context-manifest-schema.js +150 -0
- package/dist/lib/context-manifest.js +28 -20
- package/dist/lib/context-templates.js +5 -5
- package/dist/lib/design-md.js +1 -1
- package/dist/lib/doctor.js +54 -1
- package/dist/lib/harness-root.js +25 -8
- package/dist/lib/init.js +6 -3
- package/dist/lib/legacy-managed-scan.js +23 -7
- package/dist/lib/legacy-sdlc-migration.js +21 -11
- package/dist/lib/long-task-acceptance-reference.d.ts +22 -0
- package/dist/lib/long-task-acceptance-reference.js +46 -0
- package/dist/lib/long-task-acceptance-shape.d.ts +5 -0
- package/dist/lib/long-task-acceptance-shape.js +131 -0
- package/dist/lib/long-task-activation-validation.d.ts +21 -0
- package/dist/lib/long-task-activation-validation.js +136 -0
- package/dist/lib/long-task-artifacts.d.ts +5 -0
- package/dist/lib/long-task-artifacts.js +34 -0
- package/dist/lib/long-task-assertions-v2.d.ts +22 -0
- package/dist/lib/long-task-assertions-v2.js +212 -0
- package/dist/lib/long-task-authoring-authority-preview.d.ts +5 -0
- package/dist/lib/long-task-authoring-authority-preview.js +85 -0
- package/dist/lib/long-task-authoring-preflight-diagnostics.d.ts +8 -0
- package/dist/lib/long-task-authoring-preflight-diagnostics.js +179 -0
- package/dist/lib/long-task-authoring-preflight-repair-order.d.ts +8 -0
- package/dist/lib/long-task-authoring-preflight-repair-order.js +95 -0
- package/dist/lib/long-task-authoring-preflight-types.d.ts +46 -0
- package/dist/lib/long-task-authoring-preflight-types.js +38 -0
- package/dist/lib/long-task-authoring-preflight.d.ts +3 -0
- package/dist/lib/long-task-authoring-preflight.js +44 -0
- package/dist/lib/long-task-authority-material-diff.d.ts +17 -0
- package/dist/lib/long-task-authority-material-diff.js +152 -0
- package/dist/lib/long-task-authority-materials.d.ts +7 -0
- package/dist/lib/long-task-authority-materials.js +83 -0
- package/dist/lib/long-task-authority-policy.d.ts +168 -0
- package/dist/lib/long-task-authority-policy.js +181 -0
- package/dist/lib/long-task-authority-revision-details.d.ts +19 -0
- package/dist/lib/long-task-authority-revision-details.js +192 -0
- package/dist/lib/long-task-authority-revision-enforcement.d.ts +3 -0
- package/dist/lib/long-task-authority-revision-enforcement.js +41 -0
- package/dist/lib/long-task-authority-revision-types.d.ts +50 -0
- package/dist/lib/long-task-authority-revision-types.js +1 -0
- package/dist/lib/long-task-authority-revision.d.ts +4 -0
- package/dist/lib/long-task-authority-revision.js +188 -0
- package/dist/lib/long-task-authority-transition-policy.d.ts +62 -0
- package/dist/lib/long-task-authority-transition-policy.js +62 -0
- package/dist/lib/long-task-authority-types.d.ts +96 -0
- package/dist/lib/long-task-authority-types.js +1 -0
- package/dist/lib/long-task-authority.d.ts +7 -0
- package/dist/lib/long-task-authority.js +142 -0
- package/dist/lib/long-task-boundary-check.d.ts +15 -0
- package/dist/lib/long-task-boundary-check.js +9 -0
- package/dist/lib/long-task-check-evidence-decoder.d.ts +3 -0
- package/dist/lib/long-task-check-evidence-decoder.js +74 -0
- package/dist/lib/long-task-check-execution-policy.d.ts +18 -0
- package/dist/lib/long-task-check-execution-policy.js +36 -0
- package/dist/lib/long-task-check-runner.d.ts +2 -0
- package/dist/lib/long-task-check-runner.js +181 -0
- package/dist/lib/long-task-check-shape.d.ts +2 -0
- package/dist/lib/long-task-check-shape.js +136 -0
- package/dist/lib/long-task-claim-definitions.d.ts +7 -0
- package/dist/lib/long-task-claim-definitions.js +78 -0
- package/dist/lib/long-task-claim-proof-policy.d.ts +4 -0
- package/dist/lib/long-task-claim-proof-policy.js +57 -0
- package/dist/lib/long-task-claims.d.ts +10 -0
- package/dist/lib/long-task-claims.js +187 -0
- package/dist/lib/long-task-context-authority-topology.d.ts +7 -0
- package/dist/lib/long-task-context-authority-topology.js +37 -0
- package/dist/lib/long-task-context-authority.d.ts +14 -0
- package/dist/lib/long-task-context-authority.js +66 -0
- package/dist/lib/long-task-contract-types.d.ts +203 -0
- package/dist/lib/long-task-contract-types.js +1 -0
- package/dist/lib/long-task-counterfactual-claim-policy.d.ts +10 -0
- package/dist/lib/long-task-counterfactual-claim-policy.js +57 -0
- package/dist/lib/long-task-counterfactual-sandbox.d.ts +6 -0
- package/dist/lib/long-task-counterfactual-sandbox.js +87 -0
- package/dist/lib/long-task-counterfactual-types.d.ts +24 -0
- package/dist/lib/long-task-counterfactual-types.js +1 -0
- package/dist/lib/long-task-delivery-compiler.d.ts +10 -0
- package/dist/lib/long-task-delivery-compiler.js +129 -0
- package/dist/lib/long-task-delivery-parser.d.ts +13 -0
- package/dist/lib/long-task-delivery-parser.js +90 -0
- package/dist/lib/long-task-delivery-preflight.d.ts +5 -0
- package/dist/lib/long-task-delivery-preflight.js +151 -0
- package/dist/lib/long-task-delivery-shape.d.ts +5 -0
- package/dist/lib/long-task-delivery-shape.js +5 -0
- package/dist/lib/long-task-delivery-types.d.ts +8 -0
- package/dist/lib/long-task-delivery-types.js +8 -0
- package/dist/lib/long-task-delivery-validation.d.ts +3 -0
- package/dist/lib/long-task-delivery-validation.js +175 -0
- package/dist/lib/long-task-evidence-adapter-policy.d.ts +4 -0
- package/dist/lib/long-task-evidence-adapter-policy.js +15 -0
- package/dist/lib/long-task-evidence-adapter-types.d.ts +1 -0
- package/dist/lib/long-task-evidence-adapter-types.js +1 -0
- package/dist/lib/long-task-evidence-findings.d.ts +5 -0
- package/dist/lib/long-task-evidence-findings.js +113 -0
- package/dist/lib/long-task-evidence-sensitivity-policy.d.ts +4 -0
- package/dist/lib/long-task-evidence-sensitivity-policy.js +98 -0
- package/dist/lib/long-task-evidence-v2.d.ts +5 -0
- package/dist/lib/long-task-evidence-v2.js +226 -0
- package/dist/lib/long-task-explain-acceptance-link.d.ts +46 -0
- package/dist/lib/long-task-explain-acceptance-link.js +96 -0
- package/dist/lib/long-task-explain-claim-links.d.ts +45 -0
- package/dist/lib/long-task-explain-claim-links.js +88 -0
- package/dist/lib/long-task-explain-source-links.d.ts +93 -0
- package/dist/lib/long-task-explain-source-links.js +51 -0
- package/dist/lib/long-task-final-v2.d.ts +2 -0
- package/dist/lib/long-task-final-v2.js +124 -0
- package/dist/lib/long-task-finding-context.d.ts +3 -0
- package/dist/lib/long-task-finding-context.js +63 -0
- package/dist/lib/long-task-freshness.d.ts +3 -0
- package/dist/lib/long-task-freshness.js +80 -0
- package/dist/lib/long-task-hook-install.d.ts +9 -0
- package/dist/lib/long-task-hook-install.js +184 -0
- package/dist/lib/long-task-hook-preflight.d.ts +7 -0
- package/dist/lib/long-task-hook-preflight.js +58 -0
- package/dist/lib/long-task-observation-ownership.d.ts +2 -0
- package/dist/lib/long-task-observation-ownership.js +22 -0
- package/dist/lib/long-task-outcome-parser.d.ts +4 -0
- package/dist/lib/long-task-outcome-parser.js +100 -0
- package/dist/lib/long-task-paths.d.ts +49 -0
- package/dist/lib/long-task-paths.js +418 -0
- package/dist/lib/long-task-playwright-counterfactual-policy.d.ts +7 -0
- package/dist/lib/long-task-playwright-counterfactual-policy.js +125 -0
- package/dist/lib/long-task-playwright-evidence.d.ts +6 -0
- package/dist/lib/long-task-playwright-evidence.js +244 -0
- package/dist/lib/long-task-product-shape.d.ts +6 -0
- package/dist/lib/long-task-product-shape.js +73 -0
- package/dist/lib/long-task-progress.d.ts +4 -0
- package/dist/lib/long-task-progress.js +114 -0
- package/dist/lib/long-task-protected-files.d.ts +1 -0
- package/dist/lib/long-task-protected-files.js +30 -0
- package/dist/lib/long-task-required-proof-surfaces.d.ts +2 -0
- package/dist/lib/long-task-required-proof-surfaces.js +13 -0
- package/dist/lib/long-task-requirement-shape.d.ts +2 -0
- package/dist/lib/long-task-requirement-shape.js +18 -0
- package/dist/lib/long-task-risk-surfaces.d.ts +2 -0
- package/dist/lib/long-task-risk-surfaces.js +60 -0
- package/dist/lib/long-task-risk-types.d.ts +5 -0
- package/dist/lib/long-task-risk-types.js +12 -0
- package/dist/lib/long-task-risk.d.ts +9 -0
- package/dist/lib/long-task-risk.js +132 -0
- package/dist/lib/long-task-root-shape.d.ts +4 -0
- package/dist/lib/long-task-root-shape.js +86 -0
- package/dist/lib/long-task-runner-environment.d.ts +7 -0
- package/dist/lib/long-task-runner-environment.js +57 -0
- package/dist/lib/long-task-runner-files.d.ts +3 -0
- package/dist/lib/long-task-runner-files.js +39 -0
- package/dist/lib/long-task-runner-freeze.d.ts +2 -0
- package/dist/lib/long-task-runner-freeze.js +168 -0
- package/dist/lib/long-task-runtime-types.d.ts +214 -0
- package/dist/lib/long-task-runtime-types.js +1 -0
- package/dist/lib/long-task-shape-primitives.d.ts +17 -0
- package/dist/lib/long-task-shape-primitives.js +81 -0
- package/dist/lib/long-task-source-authority-types.d.ts +13 -0
- package/dist/lib/long-task-source-authority-types.js +1 -0
- package/dist/lib/long-task-source-claim-validation.d.ts +5 -0
- package/dist/lib/long-task-source-claim-validation.js +96 -0
- package/dist/lib/long-task-source-continuity.d.ts +4 -0
- package/dist/lib/long-task-source-continuity.js +57 -0
- package/dist/lib/long-task-source-inventory.d.ts +2 -0
- package/dist/lib/long-task-source-inventory.js +21 -0
- package/dist/lib/long-task-source-item-parser.d.ts +3 -0
- package/dist/lib/long-task-source-item-parser.js +112 -0
- package/dist/lib/long-task-source-shape.d.ts +2 -0
- package/dist/lib/long-task-source-shape.js +56 -0
- package/dist/lib/long-task-source-target-continuity.d.ts +4 -0
- package/dist/lib/long-task-source-target-continuity.js +80 -0
- package/dist/lib/long-task-source-target-index.d.ts +12 -0
- package/dist/lib/long-task-source-target-index.js +76 -0
- package/dist/lib/long-task-source-validation.d.ts +3 -0
- package/dist/lib/long-task-source-validation.js +46 -0
- package/dist/lib/long-task-state.d.ts +91 -0
- package/dist/lib/long-task-state.js +611 -0
- package/dist/lib/long-task-status-projection.d.ts +21 -0
- package/dist/lib/long-task-status-projection.js +116 -0
- package/dist/lib/long-task-status-v2.d.ts +37 -0
- package/dist/lib/long-task-status-v2.js +306 -0
- package/dist/lib/long-task-technical-shape.d.ts +3 -0
- package/dist/lib/long-task-technical-shape.js +44 -0
- package/dist/lib/long-task-verifier-authority.d.ts +12 -0
- package/dist/lib/long-task-verifier-authority.js +47 -0
- package/dist/lib/long-task-verifier-identity.d.ts +2 -0
- package/dist/lib/long-task-verifier-identity.js +55 -0
- package/dist/lib/long-task-verifier-v2.d.ts +13 -0
- package/dist/lib/long-task-verifier-v2.js +266 -0
- package/dist/lib/long-task-workspace-runtime-types.d.ts +23 -0
- package/dist/lib/long-task-workspace-runtime-types.js +1 -0
- package/dist/lib/long-task-workspace.d.ts +25 -0
- package/dist/lib/long-task-workspace.js +366 -0
- package/dist/lib/managed-file.js +20 -5
- package/dist/lib/migrations.js +156 -32
- package/dist/lib/modularity.d.ts +27 -1
- package/dist/lib/modularity.js +499 -23
- package/dist/lib/package-json-config.js +14 -5
- package/dist/lib/package-source.js +4 -2
- package/dist/lib/profiles.d.ts +13 -0
- package/dist/lib/profiles.js +66 -0
- package/dist/lib/schema-guard.js +4 -2
- package/dist/lib/source-files.js +14 -7
- package/dist/lib/source-pack-classify.js +91 -19
- package/dist/lib/source-pack-config.js +25 -8
- package/dist/lib/source-pack-export.js +84 -25
- package/dist/lib/source-pack-manifest.js +18 -6
- package/dist/lib/source-pack-records.js +35 -11
- package/dist/lib/source-pack-render.js +47 -18
- package/dist/lib/stable-json.d.ts +2 -0
- package/dist/lib/stable-json.js +21 -0
- package/dist/lib/strict-codec.d.ts +4 -0
- package/dist/lib/strict-codec.js +65 -0
- package/dist/lib/sync-engine.js +76 -24
- package/dist/lib/types.d.ts +8 -1
- package/dist/lib/upgrade.js +4 -3
- package/dist/lib/validators.js +218 -191
- package/dist/long-task-hook.d.ts +2 -0
- package/dist/long-task-hook.js +55 -0
- package/dist/schemas/long-task-delivery-v2/long-task-delivery-v2.schema.json +340 -0
- package/dist/schemas/long-task-delivery-v2/long-task-outcomes-v2.schema.json +18 -0
- package/migrations/README.md +8 -3
- package/package.json +17 -6
- package/source-mappings.yaml +0 -3
- package/assets/protected-harness-baseline.json +0 -20
- package/assets/skills/composite-long-task-workflow/SKILL.md +0 -245
- package/assets/skills/composite-long-task-workflow/assets/execution-binding.template.md +0 -62
- package/assets/skills/composite-long-task-workflow/assets/goal-objective.template.md +0 -17
- package/assets/skills/composite-long-task-workflow/references/composite-long-task-workflow-protocol.md +0 -682
- package/dist/commands/superpowers.d.ts +0 -1
- package/dist/commands/superpowers.js +0 -8
- package/dist/lib/composite-long-task-renderer.d.ts +0 -12
- package/dist/lib/composite-long-task-renderer.js +0 -109
- package/dist/lib/plan-acceptance-artifacts.d.ts +0 -1
- package/dist/lib/plan-acceptance-artifacts.js +0 -220
- package/dist/lib/plan-acceptance-evidence.d.ts +0 -2
- package/dist/lib/plan-acceptance-evidence.js +0 -108
- package/dist/lib/plan-acceptance-json.d.ts +0 -15
- package/dist/lib/plan-acceptance-json.js +0 -133
- package/dist/lib/plan-acceptance-validator.d.ts +0 -2
- package/dist/lib/plan-acceptance-validator.js +0 -209
- package/dist/lib/plan-contract-validator.d.ts +0 -2
- package/dist/lib/plan-contract-validator.js +0 -127
- package/dist/lib/plan-validator-common.d.ts +0 -24
- package/dist/lib/plan-validator-common.js +0 -196
- package/dist/lib/superpowers-task-ac010.d.ts +0 -6
- package/dist/lib/superpowers-task-ac010.js +0 -26
- package/dist/lib/superpowers-task-assertion-normalizers.d.ts +0 -3
- package/dist/lib/superpowers-task-assertion-normalizers.js +0 -74
- package/dist/lib/superpowers-task-assertions.d.ts +0 -20
- package/dist/lib/superpowers-task-assertions.js +0 -257
- package/dist/lib/superpowers-task-attempt.d.ts +0 -4
- package/dist/lib/superpowers-task-attempt.js +0 -102
- package/dist/lib/superpowers-task-command-run-correlation.d.ts +0 -8
- package/dist/lib/superpowers-task-command-run-correlation.js +0 -103
- package/dist/lib/superpowers-task-command-specs.d.ts +0 -3
- package/dist/lib/superpowers-task-command-specs.js +0 -52
- package/dist/lib/superpowers-task-compile-diagnostics.d.ts +0 -5
- package/dist/lib/superpowers-task-compile-diagnostics.js +0 -20
- package/dist/lib/superpowers-task-compile-guards.d.ts +0 -2
- package/dist/lib/superpowers-task-compile-guards.js +0 -66
- package/dist/lib/superpowers-task-compile.d.ts +0 -6
- package/dist/lib/superpowers-task-compile.js +0 -133
- package/dist/lib/superpowers-task-completion-output.d.ts +0 -91
- package/dist/lib/superpowers-task-completion-output.js +0 -374
- package/dist/lib/superpowers-task-conformance.d.ts +0 -2
- package/dist/lib/superpowers-task-conformance.js +0 -24
- package/dist/lib/superpowers-task-contradictions.d.ts +0 -6
- package/dist/lib/superpowers-task-contradictions.js +0 -126
- package/dist/lib/superpowers-task-current-evidence.d.ts +0 -3
- package/dist/lib/superpowers-task-current-evidence.js +0 -176
- package/dist/lib/superpowers-task-delivery.d.ts +0 -4
- package/dist/lib/superpowers-task-delivery.js +0 -96
- package/dist/lib/superpowers-task-derive.d.ts +0 -14
- package/dist/lib/superpowers-task-derive.js +0 -393
- package/dist/lib/superpowers-task-events.d.ts +0 -1
- package/dist/lib/superpowers-task-events.js +0 -13
- package/dist/lib/superpowers-task-evidence-kernel.d.ts +0 -19
- package/dist/lib/superpowers-task-evidence-kernel.js +0 -347
- package/dist/lib/superpowers-task-evidence-records.d.ts +0 -2
- package/dist/lib/superpowers-task-evidence-records.js +0 -55
- package/dist/lib/superpowers-task-evidence.d.ts +0 -10
- package/dist/lib/superpowers-task-evidence.js +0 -147
- package/dist/lib/superpowers-task-fields.d.ts +0 -22
- package/dist/lib/superpowers-task-fields.js +0 -276
- package/dist/lib/superpowers-task-final-card.d.ts +0 -3
- package/dist/lib/superpowers-task-final-card.js +0 -35
- package/dist/lib/superpowers-task-gates.d.ts +0 -12
- package/dist/lib/superpowers-task-gates.js +0 -267
- package/dist/lib/superpowers-task-harness-drift.d.ts +0 -11
- package/dist/lib/superpowers-task-harness-drift.js +0 -90
- package/dist/lib/superpowers-task-next-slices.d.ts +0 -1
- package/dist/lib/superpowers-task-next-slices.js +0 -12
- package/dist/lib/superpowers-task-protected-baseline.d.ts +0 -10
- package/dist/lib/superpowers-task-protected-baseline.js +0 -66
- package/dist/lib/superpowers-task-source-compile.d.ts +0 -5
- package/dist/lib/superpowers-task-source-compile.js +0 -226
- package/dist/lib/superpowers-task-source-parser.d.ts +0 -23
- package/dist/lib/superpowers-task-source-parser.js +0 -218
- package/dist/lib/superpowers-task-state-schema.d.ts +0 -460
- package/dist/lib/superpowers-task-state-schema.js +0 -66
- package/dist/lib/superpowers-task-state-shape.d.ts +0 -3
- package/dist/lib/superpowers-task-state-shape.js +0 -50
- package/dist/lib/superpowers-task-state.d.ts +0 -16
- package/dist/lib/superpowers-task-state.js +0 -246
- package/dist/lib/superpowers-task-status.d.ts +0 -2
- package/dist/lib/superpowers-task-status.js +0 -24
- package/dist/lib/superpowers-task-under-specified.d.ts +0 -7
- package/dist/lib/superpowers-task-under-specified.js +0 -61
- package/dist/lib/superpowers-task-unregistered-evidence.d.ts +0 -11
- package/dist/lib/superpowers-task-unregistered-evidence.js +0 -72
- package/dist/lib/superpowers-task-validator.d.ts +0 -5
- package/dist/lib/superpowers-task-validator.js +0 -293
package/dist/lib/modularity.js
CHANGED
|
@@ -8,28 +8,40 @@ const execFileAsync = promisify(execFile);
|
|
|
8
8
|
const GIT_MAX_BUFFER = 16 * 1024 * 1024;
|
|
9
9
|
const DEFAULT_LINE_LIMIT = 300;
|
|
10
10
|
const DEFAULT_MODULARITY_POLICY = "scoped_waivers";
|
|
11
|
-
const MODULARITY_POLICIES = new Set([
|
|
11
|
+
const MODULARITY_POLICIES = new Set([
|
|
12
|
+
DEFAULT_MODULARITY_POLICY,
|
|
13
|
+
"strict_except_generated",
|
|
14
|
+
]);
|
|
15
|
+
const COMPLEXITY_LIMITS = {
|
|
16
|
+
functionStatements: 80,
|
|
17
|
+
branchComplexity: 25,
|
|
18
|
+
exports: 24,
|
|
19
|
+
stateTransitions: 24,
|
|
20
|
+
responsibilities: 6,
|
|
21
|
+
};
|
|
12
22
|
const GENERATED_FILE_PATTERNS = [
|
|
13
23
|
/^\s*(?:\/\/|#|--|;)\s*@generated\b/im,
|
|
14
24
|
/^\s*(?:\/\/|#|--|;)\s*code generated .*do not edit\.?\s*$/im,
|
|
15
25
|
/^\s*(?:\/\/|#|--|;)\s*do not edit\.?\s*$/im,
|
|
16
26
|
/^\s*(?:\/\/|#|--|;|<!--)\s*<auto-generated\b/im,
|
|
17
27
|
/^\s*(?:\/\/|#|--|;)\s*this file was generated\b/im,
|
|
18
|
-
/^\s*(?:\/\/|#|--|;)\s*generated by\b/im
|
|
28
|
+
/^\s*(?:\/\/|#|--|;)\s*generated by\b/im,
|
|
19
29
|
];
|
|
20
30
|
const WAIVER_CATEGORIES = new Set([
|
|
21
31
|
"generated",
|
|
22
32
|
"third_party_reference",
|
|
23
33
|
"legacy_migration",
|
|
24
34
|
"aggregate_styles",
|
|
25
|
-
"fixture_snapshot"
|
|
35
|
+
"fixture_snapshot",
|
|
26
36
|
]);
|
|
27
37
|
export async function runModularityCheck(projectRoot, options) {
|
|
28
38
|
const config = await readConfig(projectRoot);
|
|
29
|
-
const { limit: configuredLimit, waiverValues, errors: configErrors } = validateModularityConfig(config.modularity);
|
|
39
|
+
const { limit: configuredLimit, waiverValues, errors: configErrors, } = validateModularityConfig(config.modularity);
|
|
30
40
|
const { waivers, errors: waiverErrors } = validateWaivers(projectRoot, waiverValues);
|
|
31
41
|
const limit = options.limit ?? configuredLimit ?? DEFAULT_LINE_LIMIT;
|
|
42
|
+
const waiverTargetErrors = await validateWaiverTargets(projectRoot, waivers, limit);
|
|
32
43
|
const candidates = new Set();
|
|
44
|
+
const baselineRef = options.base ?? (options.touched ? "HEAD" : undefined);
|
|
33
45
|
for (const file of options.files ?? []) {
|
|
34
46
|
candidates.add(normalizeExplicitPath(projectRoot, file));
|
|
35
47
|
}
|
|
@@ -49,20 +61,149 @@ export async function runModularityCheck(projectRoot, options) {
|
|
|
49
61
|
continue;
|
|
50
62
|
}
|
|
51
63
|
const absolutePath = path.join(projectRoot, ...relativePath.split("/"));
|
|
52
|
-
if (!(await isRegularFile(absolutePath)) ||
|
|
64
|
+
if (!(await isRegularFile(absolutePath)) ||
|
|
65
|
+
(await isLikelyGeneratedFile(absolutePath))) {
|
|
53
66
|
continue;
|
|
54
67
|
}
|
|
55
|
-
const
|
|
68
|
+
const content = await fs.readFile(absolutePath, "utf8");
|
|
69
|
+
const lines = countPhysicalLines(content);
|
|
70
|
+
const metrics = analyzeModularity(content, relativePath);
|
|
71
|
+
const risks = modularityRisks(lines, limit, metrics);
|
|
72
|
+
const baselineContent = baselineRef
|
|
73
|
+
? await gitFileAtRef(projectRoot, baselineRef, relativePath)
|
|
74
|
+
: undefined;
|
|
75
|
+
const baseline = baselineContent
|
|
76
|
+
? analyzeFile(baselineContent, limit, relativePath)
|
|
77
|
+
: undefined;
|
|
78
|
+
const regressions = modularityRegressions({ lines, metrics, risks }, baseline);
|
|
56
79
|
const waiver = waivers.get(relativePath);
|
|
57
|
-
files.push({
|
|
80
|
+
files.push({
|
|
81
|
+
relativePath,
|
|
82
|
+
lines,
|
|
83
|
+
metrics,
|
|
84
|
+
risks,
|
|
85
|
+
regressions,
|
|
86
|
+
overLimit: risks.length > 0,
|
|
87
|
+
regressed: regressions.length > 0,
|
|
88
|
+
baseline,
|
|
89
|
+
waived: waiver,
|
|
90
|
+
});
|
|
58
91
|
}
|
|
59
92
|
const warnings = files
|
|
60
|
-
.filter((file) => file.
|
|
61
|
-
.map((file) => `${file.relativePath}: ${file.
|
|
93
|
+
.filter((file) => file.regressed && !file.waived)
|
|
94
|
+
.map((file) => `${file.relativePath}: ${file.regressions.join("; ")}`);
|
|
62
95
|
const waivedWarnings = files
|
|
63
|
-
.filter((file) => file.
|
|
64
|
-
.map((file) => `${file.relativePath}: ${file.
|
|
65
|
-
return {
|
|
96
|
+
.filter((file) => file.regressed && file.waived)
|
|
97
|
+
.map((file) => `${file.relativePath}: ${file.regressions.join("; ")} but is waived by ${file.waived?.owner} until ${file.waived?.expiryCondition}`);
|
|
98
|
+
return {
|
|
99
|
+
limit,
|
|
100
|
+
files,
|
|
101
|
+
warnings,
|
|
102
|
+
waivedWarnings,
|
|
103
|
+
errors: [...configErrors, ...waiverErrors, ...waiverTargetErrors],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function analyzeFile(content, limit, relativePath) {
|
|
107
|
+
const lines = countPhysicalLines(content);
|
|
108
|
+
const metrics = analyzeModularity(content, relativePath);
|
|
109
|
+
return { lines, metrics, risks: modularityRisks(lines, limit, metrics) };
|
|
110
|
+
}
|
|
111
|
+
function modularityRegressions(current, baseline) {
|
|
112
|
+
if (!baseline) {
|
|
113
|
+
return current.risks;
|
|
114
|
+
}
|
|
115
|
+
const checks = [
|
|
116
|
+
{
|
|
117
|
+
current: current.metrics.maxFunctionStatements,
|
|
118
|
+
baseline: baseline.metrics.maxFunctionStatements,
|
|
119
|
+
limit: COMPLEXITY_LIMITS.functionStatements,
|
|
120
|
+
message: `${current.metrics.maxFunctionStatements} statements in one function exceeds limit ${COMPLEXITY_LIMITS.functionStatements}${formatMetricLocation(current.metrics.maxFunctionStatementsLocation)}`,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
current: current.metrics.maxBranchComplexity,
|
|
124
|
+
baseline: baseline.metrics.maxBranchComplexity,
|
|
125
|
+
limit: COMPLEXITY_LIMITS.branchComplexity,
|
|
126
|
+
message: `${current.metrics.maxBranchComplexity} branch complexity exceeds limit ${COMPLEXITY_LIMITS.branchComplexity}${formatMetricLocation(current.metrics.maxBranchComplexityLocation)}`,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
current: current.metrics.exports,
|
|
130
|
+
baseline: baseline.metrics.exports,
|
|
131
|
+
limit: COMPLEXITY_LIMITS.exports,
|
|
132
|
+
message: `${current.metrics.exports} exports exceeds limit ${COMPLEXITY_LIMITS.exports}`,
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
current: current.metrics.stateTransitions,
|
|
136
|
+
baseline: baseline.metrics.stateTransitions,
|
|
137
|
+
limit: COMPLEXITY_LIMITS.stateTransitions,
|
|
138
|
+
message: `${current.metrics.stateTransitions} state transitions exceeds limit ${COMPLEXITY_LIMITS.stateTransitions}`,
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
current: current.metrics.responsibilities.length,
|
|
142
|
+
baseline: baseline.metrics.responsibilities.length,
|
|
143
|
+
limit: COMPLEXITY_LIMITS.responsibilities,
|
|
144
|
+
message: `${current.metrics.responsibilities.length} module responsibilities exceeds limit ${COMPLEXITY_LIMITS.responsibilities}`,
|
|
145
|
+
},
|
|
146
|
+
];
|
|
147
|
+
return checks
|
|
148
|
+
.filter((check) => check.current > check.limit && check.current > check.baseline)
|
|
149
|
+
.map((check) => check.message);
|
|
150
|
+
}
|
|
151
|
+
export function analyzeModularity(content, relativePath = "") {
|
|
152
|
+
const code = sanitizeCode(content);
|
|
153
|
+
const bodies = relativePath.toLowerCase().endsWith(".py")
|
|
154
|
+
? pythonFunctionBodies(code)
|
|
155
|
+
: functionBodies(code);
|
|
156
|
+
const analyzedBodies = bodies.map((body) => ({
|
|
157
|
+
...body,
|
|
158
|
+
statements: statementCount(body.body),
|
|
159
|
+
branches: branchComplexity(body.body),
|
|
160
|
+
}));
|
|
161
|
+
const statementPeak = maxMetricBody(analyzedBodies, "statements");
|
|
162
|
+
const branchPeak = maxMetricBody(analyzedBodies, "branches");
|
|
163
|
+
return {
|
|
164
|
+
maxFunctionStatements: statementPeak?.statements ?? 0,
|
|
165
|
+
...(statementPeak
|
|
166
|
+
? {
|
|
167
|
+
maxFunctionStatementsLocation: {
|
|
168
|
+
symbol: statementPeak.symbol,
|
|
169
|
+
line: statementPeak.line,
|
|
170
|
+
},
|
|
171
|
+
}
|
|
172
|
+
: {}),
|
|
173
|
+
maxBranchComplexity: branchPeak?.branches ?? 0,
|
|
174
|
+
...(branchPeak
|
|
175
|
+
? {
|
|
176
|
+
maxBranchComplexityLocation: {
|
|
177
|
+
symbol: branchPeak.symbol,
|
|
178
|
+
line: branchPeak.line,
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
: {}),
|
|
182
|
+
exports: exportCount(code),
|
|
183
|
+
stateTransitions: stateTransitionCount(code),
|
|
184
|
+
responsibilities: inferResponsibilities(code),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function modularityRisks(lines, lineLimit, metrics) {
|
|
188
|
+
const risks = [];
|
|
189
|
+
if (lines > lineLimit)
|
|
190
|
+
risks.push(`${lines} physical lines exceeds limit ${lineLimit}`);
|
|
191
|
+
if (metrics.maxFunctionStatements > COMPLEXITY_LIMITS.functionStatements) {
|
|
192
|
+
risks.push(`${metrics.maxFunctionStatements} statements in one function exceeds limit ${COMPLEXITY_LIMITS.functionStatements}${formatMetricLocation(metrics.maxFunctionStatementsLocation)}`);
|
|
193
|
+
}
|
|
194
|
+
if (metrics.maxBranchComplexity > COMPLEXITY_LIMITS.branchComplexity) {
|
|
195
|
+
risks.push(`${metrics.maxBranchComplexity} branch complexity exceeds limit ${COMPLEXITY_LIMITS.branchComplexity}${formatMetricLocation(metrics.maxBranchComplexityLocation)}`);
|
|
196
|
+
}
|
|
197
|
+
if (metrics.exports > COMPLEXITY_LIMITS.exports) {
|
|
198
|
+
risks.push(`${metrics.exports} exports exceeds limit ${COMPLEXITY_LIMITS.exports}`);
|
|
199
|
+
}
|
|
200
|
+
if (metrics.stateTransitions > COMPLEXITY_LIMITS.stateTransitions) {
|
|
201
|
+
risks.push(`${metrics.stateTransitions} state transitions exceeds limit ${COMPLEXITY_LIMITS.stateTransitions}`);
|
|
202
|
+
}
|
|
203
|
+
if (metrics.responsibilities.length > COMPLEXITY_LIMITS.responsibilities) {
|
|
204
|
+
risks.push(`${metrics.responsibilities.length} module responsibilities exceeds limit ${COMPLEXITY_LIMITS.responsibilities}`);
|
|
205
|
+
}
|
|
206
|
+
return risks;
|
|
66
207
|
}
|
|
67
208
|
export function countPhysicalLines(content) {
|
|
68
209
|
if (content.length === 0) {
|
|
@@ -74,12 +215,277 @@ export function countPhysicalLines(content) {
|
|
|
74
215
|
}
|
|
75
216
|
return lines.length;
|
|
76
217
|
}
|
|
218
|
+
function sanitizeCode(content) {
|
|
219
|
+
const cursor = {
|
|
220
|
+
state: "code",
|
|
221
|
+
result: "",
|
|
222
|
+
index: 0,
|
|
223
|
+
regexCharacterClass: false,
|
|
224
|
+
};
|
|
225
|
+
while (cursor.index < content.length) {
|
|
226
|
+
if (cursor.state === "code")
|
|
227
|
+
scanCode(content, cursor);
|
|
228
|
+
else if (cursor.state === "line-comment")
|
|
229
|
+
scanLineComment(content, cursor);
|
|
230
|
+
else if (cursor.state === "block-comment")
|
|
231
|
+
scanBlockComment(content, cursor);
|
|
232
|
+
else if (cursor.state === "regex")
|
|
233
|
+
scanRegex(content, cursor);
|
|
234
|
+
else
|
|
235
|
+
scanQuoted(content, cursor);
|
|
236
|
+
cursor.index += 1;
|
|
237
|
+
}
|
|
238
|
+
return cursor.result;
|
|
239
|
+
}
|
|
240
|
+
function scanCode(content, cursor) {
|
|
241
|
+
const char = content[cursor.index];
|
|
242
|
+
const next = content[cursor.index + 1];
|
|
243
|
+
if (char === "/" && (next === "/" || next === "*")) {
|
|
244
|
+
cursor.state = next === "/" ? "line-comment" : "block-comment";
|
|
245
|
+
cursor.result += " ";
|
|
246
|
+
cursor.index += 1;
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (char === "/" && startsRegexLiteral(cursor.result)) {
|
|
250
|
+
cursor.state = "regex";
|
|
251
|
+
cursor.regexCharacterClass = false;
|
|
252
|
+
cursor.result += " ";
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const quoteState = quoteStateFor(char);
|
|
256
|
+
if (quoteState) {
|
|
257
|
+
cursor.state = quoteState;
|
|
258
|
+
cursor.result += " ";
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
cursor.result += char;
|
|
262
|
+
}
|
|
263
|
+
function scanLineComment(content, cursor) {
|
|
264
|
+
const char = content[cursor.index];
|
|
265
|
+
if (isLineBreak(char))
|
|
266
|
+
cursor.state = "code";
|
|
267
|
+
cursor.result += isLineBreak(char) ? char : " ";
|
|
268
|
+
}
|
|
269
|
+
function scanBlockComment(content, cursor) {
|
|
270
|
+
const char = content[cursor.index];
|
|
271
|
+
if (char === "*" && content[cursor.index + 1] === "/") {
|
|
272
|
+
cursor.state = "code";
|
|
273
|
+
cursor.result += " ";
|
|
274
|
+
cursor.index += 1;
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
cursor.result += isLineBreak(char) ? char : " ";
|
|
278
|
+
}
|
|
279
|
+
function scanRegex(content, cursor) {
|
|
280
|
+
const char = content[cursor.index];
|
|
281
|
+
if (char === "\\") {
|
|
282
|
+
cursor.result += " ";
|
|
283
|
+
cursor.index += 1;
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (char === "[")
|
|
287
|
+
cursor.regexCharacterClass = true;
|
|
288
|
+
if (char === "]")
|
|
289
|
+
cursor.regexCharacterClass = false;
|
|
290
|
+
if (char === "/" && !cursor.regexCharacterClass)
|
|
291
|
+
cursor.state = "code";
|
|
292
|
+
cursor.result += isLineBreak(char) ? char : " ";
|
|
293
|
+
}
|
|
294
|
+
function scanQuoted(content, cursor) {
|
|
295
|
+
const char = content[cursor.index];
|
|
296
|
+
if (char === "\\") {
|
|
297
|
+
cursor.result += " ";
|
|
298
|
+
cursor.index += 1;
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (char === delimiterFor(cursor.state))
|
|
302
|
+
cursor.state = "code";
|
|
303
|
+
cursor.result += isLineBreak(char) ? char : " ";
|
|
304
|
+
}
|
|
305
|
+
function quoteStateFor(char) {
|
|
306
|
+
if (char === "'")
|
|
307
|
+
return "single";
|
|
308
|
+
if (char === '"')
|
|
309
|
+
return "double";
|
|
310
|
+
if (char === "`")
|
|
311
|
+
return "template";
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
function delimiterFor(state) {
|
|
315
|
+
if (state === "single")
|
|
316
|
+
return "'";
|
|
317
|
+
if (state === "double")
|
|
318
|
+
return '"';
|
|
319
|
+
return "`";
|
|
320
|
+
}
|
|
321
|
+
function isLineBreak(char) {
|
|
322
|
+
return char === "\n" || char === "\r";
|
|
323
|
+
}
|
|
324
|
+
function startsRegexLiteral(result) {
|
|
325
|
+
const trimmed = result.trimEnd();
|
|
326
|
+
if (!trimmed)
|
|
327
|
+
return true;
|
|
328
|
+
const previous = trimmed.at(-1) ?? "";
|
|
329
|
+
if (/[=(:,!&|?;\[{]/u.test(previous))
|
|
330
|
+
return true;
|
|
331
|
+
return /\b(?:return|throw|case|delete|void|typeof|instanceof|in|of)$/u.test(trimmed);
|
|
332
|
+
}
|
|
333
|
+
function functionBodies(code) {
|
|
334
|
+
const starts = new Map();
|
|
335
|
+
const add = (openingBrace, symbol) => {
|
|
336
|
+
const existing = starts.get(openingBrace);
|
|
337
|
+
if (!existing || existing === "<anonymous>")
|
|
338
|
+
starts.set(openingBrace, symbol);
|
|
339
|
+
};
|
|
340
|
+
for (const match of code.matchAll(/\bfunction\s*([A-Za-z_$][\w$]*)?[^{};]*\{/g)) {
|
|
341
|
+
const openingBrace = (match.index ?? 0) + match[0].lastIndexOf("{");
|
|
342
|
+
add(openingBrace, match[1] || "<anonymous>");
|
|
343
|
+
}
|
|
344
|
+
for (const match of code.matchAll(/=>\s*\{/g)) {
|
|
345
|
+
const openingBrace = (match.index ?? 0) + match[0].lastIndexOf("{");
|
|
346
|
+
add(openingBrace, inferArrowSymbol(code, match.index ?? 0));
|
|
347
|
+
}
|
|
348
|
+
for (const match of code.matchAll(/\b(?:async\s+)?(?:get\s+|set\s+)?([A-Za-z_$][\w$]*)\s*\([^;{}]*\)\s*(?::\s*[^={]+)?\{/g)) {
|
|
349
|
+
const prefix = match[0].trimStart();
|
|
350
|
+
if (/^(?:if|for|while|switch|catch|with)\b/u.test(prefix))
|
|
351
|
+
continue;
|
|
352
|
+
const openingBrace = (match.index ?? 0) + match[0].lastIndexOf("{");
|
|
353
|
+
add(openingBrace, match[1] || "<anonymous>");
|
|
354
|
+
}
|
|
355
|
+
return [...starts.entries()]
|
|
356
|
+
.sort(([left], [right]) => left - right)
|
|
357
|
+
.map(([openingBrace, symbol]) => {
|
|
358
|
+
const body = balancedBody(code, openingBrace);
|
|
359
|
+
return body === undefined
|
|
360
|
+
? undefined
|
|
361
|
+
: {
|
|
362
|
+
body,
|
|
363
|
+
symbol,
|
|
364
|
+
line: lineNumberAt(code, openingBrace),
|
|
365
|
+
};
|
|
366
|
+
})
|
|
367
|
+
.filter((body) => body !== undefined);
|
|
368
|
+
}
|
|
369
|
+
function pythonFunctionBodies(code) {
|
|
370
|
+
const lines = code.split(/\r\n|\n|\r/u);
|
|
371
|
+
const bodies = [];
|
|
372
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
373
|
+
const match = /^(\s*)(?:async\s+)?def\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/u.exec(lines[index]);
|
|
374
|
+
if (!match)
|
|
375
|
+
continue;
|
|
376
|
+
const indentation = indentationWidth(match[1]);
|
|
377
|
+
const body = [];
|
|
378
|
+
for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
|
|
379
|
+
const line = lines[cursor];
|
|
380
|
+
if (!line.trim()) {
|
|
381
|
+
body.push(line);
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
const leading = /^\s*/u.exec(line)?.[0] ?? "";
|
|
385
|
+
if (indentationWidth(leading) <= indentation)
|
|
386
|
+
break;
|
|
387
|
+
body.push(line);
|
|
388
|
+
}
|
|
389
|
+
bodies.push({ body: body.join("\n"), symbol: match[2], line: index + 1 });
|
|
390
|
+
}
|
|
391
|
+
return bodies;
|
|
392
|
+
}
|
|
393
|
+
function inferArrowSymbol(code, arrowIndex) {
|
|
394
|
+
const prefix = code.slice(Math.max(0, arrowIndex - 240), arrowIndex);
|
|
395
|
+
const assignment = /(?:\b(?:const|let|var)\s+|\b)([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*$/u.exec(prefix);
|
|
396
|
+
if (assignment?.[1])
|
|
397
|
+
return assignment[1];
|
|
398
|
+
const property = /([A-Za-z_$][\w$]*)\s*:\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*$/u.exec(prefix);
|
|
399
|
+
return property?.[1] ?? "<anonymous>";
|
|
400
|
+
}
|
|
401
|
+
function lineNumberAt(code, index) {
|
|
402
|
+
return code.slice(0, index).split(/\r\n|\n|\r/u).length;
|
|
403
|
+
}
|
|
404
|
+
function maxMetricBody(bodies, metric) {
|
|
405
|
+
let selected;
|
|
406
|
+
for (const body of bodies) {
|
|
407
|
+
if (!selected || body[metric] > selected[metric])
|
|
408
|
+
selected = body;
|
|
409
|
+
}
|
|
410
|
+
return selected;
|
|
411
|
+
}
|
|
412
|
+
function formatMetricLocation(location) {
|
|
413
|
+
return location
|
|
414
|
+
? ` in function ${location.symbol} at line ${location.line}`
|
|
415
|
+
: "";
|
|
416
|
+
}
|
|
417
|
+
function indentationWidth(value) {
|
|
418
|
+
return [...value].reduce((width, character) => width + (character === "\t" ? 4 : 1), 0);
|
|
419
|
+
}
|
|
420
|
+
function balancedBody(code, openingBrace) {
|
|
421
|
+
let depth = 0;
|
|
422
|
+
for (let index = openingBrace; index < code.length; index += 1) {
|
|
423
|
+
if (code[index] === "{")
|
|
424
|
+
depth += 1;
|
|
425
|
+
if (code[index] === "}")
|
|
426
|
+
depth -= 1;
|
|
427
|
+
if (depth === 0)
|
|
428
|
+
return code.slice(openingBrace + 1, index);
|
|
429
|
+
}
|
|
430
|
+
return undefined;
|
|
431
|
+
}
|
|
432
|
+
function statementCount(body) {
|
|
433
|
+
const semicolons = body.match(/;/g)?.length ?? 0;
|
|
434
|
+
const structural = body.match(/\b(?:if|for|while|switch|case|catch|return|throw|break|continue)\b/g)?.length ?? 0;
|
|
435
|
+
return semicolons + structural;
|
|
436
|
+
}
|
|
437
|
+
function branchComplexity(body) {
|
|
438
|
+
const branches = body.match(/\b(?:if|for|while|case|catch)\b|\?|&&|\|\||\?\?/g)?.length ?? 0;
|
|
439
|
+
return 1 + branches;
|
|
440
|
+
}
|
|
441
|
+
function exportCount(code) {
|
|
442
|
+
let count = code.match(/\bexport\s+(?:default\s+)?(?:async\s+)?(?:function|class|const|let|var|interface|type|enum|namespace)\b/g)?.length ?? 0;
|
|
443
|
+
for (const match of code.matchAll(/\bexport\s*\{([^}]*)\}/g)) {
|
|
444
|
+
count += match[1].split(",").filter((item) => item.trim()).length;
|
|
445
|
+
}
|
|
446
|
+
return count;
|
|
447
|
+
}
|
|
448
|
+
function stateTransitionCount(code) {
|
|
449
|
+
return (code.match(/\.(?:status|state|phase|workflow_status|campaign_status)\s*=|\b(?:transition|setState|setStatus|advanceState|mutateCampaign)[A-Za-z0-9_$]*\s*\(/g)?.length ?? 0);
|
|
450
|
+
}
|
|
451
|
+
function inferResponsibilities(code) {
|
|
452
|
+
const patterns = [
|
|
453
|
+
[
|
|
454
|
+
"filesystem",
|
|
455
|
+
/\b(?:readFile|writeFile|mkdir|rm|rename|readdir|stat|realpath)\s*\(/,
|
|
456
|
+
],
|
|
457
|
+
["process-or-git", /\b(?:spawn|execFile|runGit|process\.)/],
|
|
458
|
+
["network", /\b(?:fetch|http\.|https\.|WebSocket|net\.)/],
|
|
459
|
+
["parsing", /\b(?:parse|decode|deserialize)[A-Za-z0-9_$]*\s*\(/],
|
|
460
|
+
["validation", /\b(?:assert|validate|reject)[A-Za-z0-9_$]*\s*\(/],
|
|
461
|
+
[
|
|
462
|
+
"state-machine",
|
|
463
|
+
/\.(?:status|state|phase)\s*=|\btransition[A-Za-z0-9_$]*\s*\(/,
|
|
464
|
+
],
|
|
465
|
+
["rendering", /\b(?:render|stringify|format)[A-Za-z0-9_$]*\s*\(/],
|
|
466
|
+
[
|
|
467
|
+
"persistence",
|
|
468
|
+
/\b(?:atomic|transaction|journal|lease|checkpoint)[A-Za-z0-9_$]*\b/i,
|
|
469
|
+
],
|
|
470
|
+
["cli", /\b(?:process\.argv|console\.|parseArgs|helpText)\b/],
|
|
471
|
+
];
|
|
472
|
+
return patterns
|
|
473
|
+
.filter(([, pattern]) => pattern.test(code))
|
|
474
|
+
.map(([name]) => name);
|
|
475
|
+
}
|
|
77
476
|
async function gitTouchedFiles(projectRoot) {
|
|
78
477
|
let result;
|
|
79
478
|
try {
|
|
80
|
-
result = await execFileAsync("git", [
|
|
479
|
+
result = await execFileAsync("git", [
|
|
480
|
+
"-C",
|
|
481
|
+
projectRoot,
|
|
482
|
+
"status",
|
|
483
|
+
"--porcelain=v1",
|
|
484
|
+
"-z",
|
|
485
|
+
"--untracked-files=all",
|
|
486
|
+
], {
|
|
81
487
|
encoding: "utf8",
|
|
82
|
-
maxBuffer: GIT_MAX_BUFFER
|
|
488
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
83
489
|
});
|
|
84
490
|
}
|
|
85
491
|
catch (error) {
|
|
@@ -106,10 +512,28 @@ async function gitTouchedFiles(projectRoot) {
|
|
|
106
512
|
async function gitDiffFiles(projectRoot, base) {
|
|
107
513
|
const result = await execFileAsync("git", ["-C", projectRoot, "diff", "--name-only", "-z", base, "--"], {
|
|
108
514
|
encoding: "utf8",
|
|
109
|
-
maxBuffer: GIT_MAX_BUFFER
|
|
515
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
110
516
|
});
|
|
111
517
|
return result.stdout.split("\0").filter(Boolean);
|
|
112
518
|
}
|
|
519
|
+
async function gitFileAtRef(projectRoot, ref, relativePath) {
|
|
520
|
+
try {
|
|
521
|
+
const result = await execFileAsync("git", ["-C", projectRoot, "show", `${ref}:${relativePath}`], {
|
|
522
|
+
encoding: "utf8",
|
|
523
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
524
|
+
});
|
|
525
|
+
return result.stdout;
|
|
526
|
+
}
|
|
527
|
+
catch (error) {
|
|
528
|
+
const stderr = error && typeof error === "object" && "stderr" in error
|
|
529
|
+
? String(error.stderr)
|
|
530
|
+
: "";
|
|
531
|
+
if (/does not exist in|exists on disk, but not in|path .* not in/i.test(stderr)) {
|
|
532
|
+
return undefined;
|
|
533
|
+
}
|
|
534
|
+
throw error;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
113
537
|
function isNotGitRepositoryError(error) {
|
|
114
538
|
if (!error || typeof error !== "object") {
|
|
115
539
|
return false;
|
|
@@ -118,9 +542,13 @@ function isNotGitRepositoryError(error) {
|
|
|
118
542
|
return /not a git repository/i.test(stderr);
|
|
119
543
|
}
|
|
120
544
|
function normalizeExplicitPath(projectRoot, value) {
|
|
121
|
-
const absolute = path.isAbsolute(value)
|
|
545
|
+
const absolute = path.isAbsolute(value)
|
|
546
|
+
? path.resolve(value)
|
|
547
|
+
: path.resolve(projectRoot, value);
|
|
122
548
|
const relative = toPosix(path.relative(projectRoot, absolute));
|
|
123
|
-
if (relative.startsWith("../") ||
|
|
549
|
+
if (relative.startsWith("../") ||
|
|
550
|
+
relative === ".." ||
|
|
551
|
+
path.isAbsolute(relative)) {
|
|
124
552
|
throw new Error(`check-modularity --file must stay inside the project root: ${value}`);
|
|
125
553
|
}
|
|
126
554
|
return normalizeGitPath(relative);
|
|
@@ -133,7 +561,9 @@ function validateModularityConfig(modularity) {
|
|
|
133
561
|
if (modularity === undefined) {
|
|
134
562
|
return { errors };
|
|
135
563
|
}
|
|
136
|
-
if (!modularity ||
|
|
564
|
+
if (!modularity ||
|
|
565
|
+
typeof modularity !== "object" ||
|
|
566
|
+
Array.isArray(modularity)) {
|
|
137
567
|
errors.push("<harnessRoot>/config.yaml modularity must be an object");
|
|
138
568
|
return { errors };
|
|
139
569
|
}
|
|
@@ -149,7 +579,8 @@ function validateModularityConfig(modularity) {
|
|
|
149
579
|
}
|
|
150
580
|
}
|
|
151
581
|
if (value.policy !== undefined) {
|
|
152
|
-
if (typeof value.policy !== "string" ||
|
|
582
|
+
if (typeof value.policy !== "string" ||
|
|
583
|
+
!MODULARITY_POLICIES.has(value.policy)) {
|
|
153
584
|
errors.push("<harnessRoot>/config.yaml modularity.policy must be one of scoped_waivers, strict_except_generated");
|
|
154
585
|
}
|
|
155
586
|
else {
|
|
@@ -159,7 +590,11 @@ function validateModularityConfig(modularity) {
|
|
|
159
590
|
if (policy === "strict_except_generated" && value.waivers !== undefined) {
|
|
160
591
|
errors.push("<harnessRoot>/config.yaml modularity.waivers is not allowed when modularity.policy is strict_except_generated");
|
|
161
592
|
}
|
|
162
|
-
return {
|
|
593
|
+
return {
|
|
594
|
+
limit,
|
|
595
|
+
waiverValues: policy === "scoped_waivers" ? value.waivers : undefined,
|
|
596
|
+
errors,
|
|
597
|
+
};
|
|
163
598
|
}
|
|
164
599
|
function validateWaivers(projectRoot, waiverValues) {
|
|
165
600
|
const waivers = new Map();
|
|
@@ -179,9 +614,23 @@ function validateWaivers(projectRoot, waiverValues) {
|
|
|
179
614
|
}
|
|
180
615
|
const relativePath = requiredWaiverString(waiver, "path", label, errors);
|
|
181
616
|
const category = requiredWaiverString(waiver, "category", label, errors);
|
|
617
|
+
const owner = requiredWaiverString(waiver, "owner", label, errors);
|
|
618
|
+
const introducedAt = requiredWaiverString(waiver, "introduced_at", label, errors);
|
|
182
619
|
const reason = requiredWaiverString(waiver, "reason", label, errors);
|
|
183
|
-
const
|
|
184
|
-
|
|
620
|
+
const trackingIssue = requiredWaiverString(waiver, "tracking_issue", label, errors);
|
|
621
|
+
const expiryCondition = requiredWaiverString(waiver, "expiry_condition", label, errors);
|
|
622
|
+
if (!relativePath ||
|
|
623
|
+
!category ||
|
|
624
|
+
!owner ||
|
|
625
|
+
!introducedAt ||
|
|
626
|
+
!reason ||
|
|
627
|
+
!trackingIssue ||
|
|
628
|
+
!expiryCondition) {
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
if (!/^\d{4}-\d{2}-\d{2}$/u.test(introducedAt) ||
|
|
632
|
+
Number.isNaN(Date.parse(`${introducedAt}T00:00:00Z`))) {
|
|
633
|
+
errors.push(`${label}.introduced_at must be an ISO YYYY-MM-DD date`);
|
|
185
634
|
continue;
|
|
186
635
|
}
|
|
187
636
|
if (!WAIVER_CATEGORIES.has(category)) {
|
|
@@ -204,12 +653,39 @@ function validateWaivers(projectRoot, waiverValues) {
|
|
|
204
653
|
waivers.set(normalized, {
|
|
205
654
|
relativePath: normalized,
|
|
206
655
|
category,
|
|
656
|
+
owner,
|
|
657
|
+
introducedAt,
|
|
207
658
|
reason,
|
|
208
|
-
|
|
659
|
+
trackingIssue,
|
|
660
|
+
expiryCondition,
|
|
209
661
|
});
|
|
210
662
|
}
|
|
211
663
|
return { waivers, errors };
|
|
212
664
|
}
|
|
665
|
+
async function validateWaiverTargets(projectRoot, waivers, limit) {
|
|
666
|
+
const errors = [];
|
|
667
|
+
for (const waiver of waivers.values()) {
|
|
668
|
+
const label = `<harnessRoot>/config.yaml modularity waiver for ${waiver.relativePath}`;
|
|
669
|
+
if (!shouldIncludeCodeFile(waiver.relativePath)) {
|
|
670
|
+
errors.push(`${label} does not target a supported handwritten source file`);
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
const target = path.join(projectRoot, ...waiver.relativePath.split("/"));
|
|
674
|
+
if (!(await isRegularFile(target))) {
|
|
675
|
+
errors.push(`${label} references a missing source file`);
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
if (await isLikelyGeneratedFile(target)) {
|
|
679
|
+
errors.push(`${label} is unnecessary because generated files are excluded`);
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
const analyzed = analyzeFile(await fs.readFile(target, "utf8"), limit, waiver.relativePath);
|
|
683
|
+
if (analyzed.risks.length === 0) {
|
|
684
|
+
errors.push(`${label} is unnecessary because the source does not currently exceed a modularity threshold`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return errors;
|
|
688
|
+
}
|
|
213
689
|
function requiredWaiverString(waiver, field, label, errors) {
|
|
214
690
|
const value = waiver[field];
|
|
215
691
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
@@ -12,19 +12,28 @@ export async function packageHarnessRoot(projectRoot) {
|
|
|
12
12
|
return undefined;
|
|
13
13
|
}
|
|
14
14
|
const value = config.harnessFolderName;
|
|
15
|
-
return typeof value === "string" && value.trim()
|
|
15
|
+
return typeof value === "string" && value.trim()
|
|
16
|
+
? normalizeHarnessFolderName(value)
|
|
17
|
+
: undefined;
|
|
16
18
|
}
|
|
17
19
|
export async function writePackageHarnessRoot(projectRoot, folderName) {
|
|
18
20
|
const normalized = normalizeHarnessFolderName(folderName);
|
|
19
21
|
const packagePath = path.join(projectRoot, "package.json");
|
|
20
|
-
const packageJson = (await pathExists(packagePath))
|
|
22
|
+
const packageJson = (await pathExists(packagePath))
|
|
23
|
+
? parsePackageJson(await readText(packagePath))
|
|
24
|
+
: {};
|
|
21
25
|
const existingConfig = packageJson.tyContext;
|
|
22
|
-
const nextConfig = existingConfig &&
|
|
23
|
-
|
|
26
|
+
const nextConfig = existingConfig &&
|
|
27
|
+
typeof existingConfig === "object" &&
|
|
28
|
+
!Array.isArray(existingConfig)
|
|
29
|
+
? {
|
|
30
|
+
...existingConfig,
|
|
31
|
+
harnessFolderName: normalized,
|
|
32
|
+
}
|
|
24
33
|
: { harnessFolderName: normalized };
|
|
25
34
|
const next = {
|
|
26
35
|
...packageJson,
|
|
27
|
-
tyContext: nextConfig
|
|
36
|
+
tyContext: nextConfig,
|
|
28
37
|
};
|
|
29
38
|
return writeTextIfChanged(packagePath, `${JSON.stringify(next, null, 2)}\n`);
|
|
30
39
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { promises as fs } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { ensureDir, listFiles, pathExists, readText, writeTextIfChanged } from "./fs.js";
|
|
4
|
+
import { ensureDir, listFiles, pathExists, readText, writeTextIfChanged, } from "./fs.js";
|
|
5
5
|
import { AGENTS_BLOCK_MARKERS } from "./managed-file.js";
|
|
6
6
|
import { SOURCE_MAPPINGS_PATH } from "./paths.js";
|
|
7
7
|
import { parseYaml } from "./yaml.js";
|
|
@@ -29,7 +29,9 @@ export async function checkSource(projectRoot) {
|
|
|
29
29
|
for (const item of expected) {
|
|
30
30
|
sourceHashes.set(item.relative, hash(item.content));
|
|
31
31
|
const targetFile = path.join(target, item.relative);
|
|
32
|
-
const existing = (await pathExists(targetFile))
|
|
32
|
+
const existing = (await pathExists(targetFile))
|
|
33
|
+
? await readText(targetFile)
|
|
34
|
+
: "";
|
|
33
35
|
if (hash(existing) !== hash(item.content)) {
|
|
34
36
|
drift.push(`${mapping.target}/${item.relative}`);
|
|
35
37
|
}
|