@mnstry/atelier 0.2.0-alpha.12 → 0.2.0-alpha.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +894 -0
- package/README.md +159 -6
- package/contracts/atelier-adoption-policy.v2.schema.json +55 -0
- package/contracts/atelier-architecture.v1.schema.json +258 -0
- package/contracts/atelier-boundary-delegations.v1.schema.json +161 -0
- package/contracts/atelier-boundary-owner-keys.v1.schema.json +98 -0
- package/contracts/atelier-build.v1.schema.json +1076 -0
- package/contracts/atelier-capability.v1.schema.json +1252 -0
- package/contracts/atelier-coordination.v1.schema.json +1050 -0
- package/contracts/atelier-evidence-navigation.v1.schema.json +603 -0
- package/contracts/atelier-harness.v1.schema.json +234 -0
- package/contracts/atelier-ingestion.v1.schema.json +806 -0
- package/contracts/atelier-inquiry.v1.schema.json +1772 -0
- package/contracts/atelier-instruction-adoption.v1.schema.json +490 -0
- package/contracts/atelier-interaction.v1.schema.json +553 -0
- package/contracts/atelier-judgment.v1.schema.json +690 -0
- package/contracts/atelier-knowledge.v1.schema.json +1512 -0
- package/contracts/atelier-learning.v1.schema.json +1095 -0
- package/contracts/atelier-migration.v3.schema.json +64 -0
- package/contracts/atelier-personal-workspace-manifest.v1.schema.json +87 -0
- package/contracts/atelier-personal-workspace-overlay.v1.schema.json +299 -0
- package/contracts/atelier-template-adoption.v1.schema.json +223 -0
- package/contracts/atelier-template-profile.v1.schema.json +1168 -0
- package/contracts/atelier-trackable.v1.schema.json +781 -0
- package/contracts/atelier-upgrade-plan.v3.schema.json +532 -0
- package/docs/architecture.md +89 -0
- package/docs/assurance-controls.md +2 -2
- package/docs/blocks/capability-stewardship.md +17 -0
- package/docs/blocks/claims.md +16 -3
- package/docs/blocks/inquiry-harnesses.md +19 -0
- package/docs/blocks/learning-harnesses.md +17 -0
- package/docs/capability-stewardship.md +267 -0
- package/docs/coordination.md +51 -0
- package/docs/decision-practices.md +111 -0
- package/docs/decisions.md +29 -0
- package/docs/evidence-navigation.md +237 -0
- package/docs/exact-upgrades.md +3 -2
- package/docs/ingestion.md +181 -0
- package/docs/inquiry-harnesses.md +210 -0
- package/docs/install.md +80 -3
- package/docs/intake-and-guides.md +5 -0
- package/docs/integration-contract-decisions.md +24 -2
- package/docs/interaction-and-reflection.md +72 -0
- package/docs/knowledge-graph.md +20 -0
- package/docs/knowledge-health/api.md +30 -0
- package/docs/knowledge-health/assessment/api.md +96 -0
- package/docs/knowledge-health/assessment/capabilities.md +40 -0
- package/docs/knowledge-health/assessment/evaluation.md +27 -0
- package/docs/knowledge-health/assessment/facilitator.md +30 -0
- package/docs/knowledge-health/assessment/quickstart.md +61 -0
- package/docs/knowledge-health/assessment/troubleshooting.md +23 -0
- package/docs/knowledge-health/devday/facilitator.md +16 -0
- package/docs/knowledge-health/devday/participant-response.md +22 -0
- package/docs/knowledge-health/devday/public-default.md +107 -0
- package/docs/knowledge-health/devday/quickstart.md +18 -0
- package/docs/knowledge-health/devday/support.md +22 -0
- package/docs/knowledge-health/exercise.md +17 -0
- package/docs/knowledge-health/facilitator.md +20 -0
- package/docs/knowledge-health/quickstart.md +94 -0
- package/docs/knowledge-health/readme.md +15 -0
- package/docs/knowledge-health/support.md +34 -0
- package/docs/knowledge-setup.md +248 -0
- package/docs/knowledge-workspace.md +264 -0
- package/docs/learning-harnesses.md +237 -0
- package/docs/learning.md +215 -0
- package/docs/local-services.md +223 -19
- package/docs/obsidian-contract.md +246 -15
- package/docs/obsidian-plugin.md +252 -7
- package/docs/obsidian.md +874 -52
- package/docs/optional-ingestion.md +83 -0
- package/docs/personal-workspace.md +266 -0
- package/docs/practical-judgment.md +92 -0
- package/docs/progressive-ingestion-acceptance.md +89 -0
- package/docs/progressive-ingestion.md +235 -0
- package/docs/project-command-inventory.md +1 -0
- package/docs/release-engineering.md +50 -0
- package/docs/repo-boundary-guard.md +120 -3
- package/docs/research-starter-quickstart.md +66 -0
- package/docs/research-starter.md +114 -0
- package/docs/semantic-ingestion.md +198 -0
- package/docs/semantic-operation.md +318 -0
- package/docs/skill-steward.md +6 -0
- package/docs/template-profile.md +164 -0
- package/docs/template-upgrades.md +114 -0
- package/docs/trackables.md +111 -0
- package/docs/upgrade.md +219 -2
- package/fixtures/atelier-architecture/binding/invalid/document.json +19 -0
- package/fixtures/atelier-architecture/binding/valid/document.json +19 -0
- package/fixtures/atelier-architecture/catalog/invalid/document.json +290 -0
- package/fixtures/atelier-architecture/catalog/valid/document.json +343 -0
- package/fixtures/atelier-build-contract/attempt/invalid/document.json +20 -0
- package/fixtures/atelier-build-contract/attempt/valid/document.json +19 -0
- package/fixtures/atelier-build-contract/candidate/invalid/document.json +24 -0
- package/fixtures/atelier-build-contract/candidate/valid/document.json +23 -0
- package/fixtures/atelier-build-contract/decision/invalid/document.json +31 -0
- package/fixtures/atelier-build-contract/decision/valid/document.json +30 -0
- package/fixtures/atelier-build-contract/delivery/invalid/document.json +18 -0
- package/fixtures/atelier-build-contract/delivery/valid/document.json +17 -0
- package/fixtures/atelier-build-contract/gate/invalid/document.json +22 -0
- package/fixtures/atelier-build-contract/gate/valid/document.json +21 -0
- package/fixtures/atelier-build-contract/ledger/invalid/document.json +270 -0
- package/fixtures/atelier-build-contract/ledger/valid/document.json +269 -0
- package/fixtures/atelier-build-contract/objective/invalid/document.json +73 -0
- package/fixtures/atelier-build-contract/objective/valid/document.json +72 -0
- package/fixtures/atelier-build-contract/progress/invalid/document.json +18 -0
- package/fixtures/atelier-build-contract/progress/valid/document.json +17 -0
- package/fixtures/atelier-capability-contract/adoption/invalid/document.json +71 -0
- package/fixtures/atelier-capability-contract/adoption/valid/document.json +70 -0
- package/fixtures/atelier-capability-contract/event/invalid/document.json +18 -0
- package/fixtures/atelier-capability-contract/event/valid/document.json +17 -0
- package/fixtures/atelier-capability-contract/journal/invalid/document.json +382 -0
- package/fixtures/atelier-capability-contract/journal/valid/document.json +381 -0
- package/fixtures/atelier-capability-contract/notice/invalid/document.json +8 -0
- package/fixtures/atelier-capability-contract/notice/valid/document.json +7 -0
- package/fixtures/atelier-capability-contract/package/invalid/document.json +59 -0
- package/fixtures/atelier-capability-contract/package/valid/document.json +58 -0
- package/fixtures/atelier-capability-contract/plan/invalid/document.json +107 -0
- package/fixtures/atelier-capability-contract/plan/valid/document.json +106 -0
- package/fixtures/atelier-capability-contract/release/invalid/document.json +85 -0
- package/fixtures/atelier-capability-contract/release/valid/document.json +84 -0
- package/fixtures/atelier-capability-contract/state/invalid/document.json +316 -0
- package/fixtures/atelier-capability-contract/state/valid/document.json +315 -0
- package/fixtures/atelier-coordination/dependency/invalid/document.json +37 -0
- package/fixtures/atelier-coordination/dependency/valid/document.json +36 -0
- package/fixtures/atelier-coordination/directive/invalid/document.json +25 -0
- package/fixtures/atelier-coordination/directive/valid/document.json +24 -0
- package/fixtures/atelier-coordination/disposition/invalid/document.json +34 -0
- package/fixtures/atelier-coordination/disposition/valid/document.json +33 -0
- package/fixtures/atelier-coordination/outcome/invalid/document.json +39 -0
- package/fixtures/atelier-coordination/outcome/valid/document.json +38 -0
- package/fixtures/atelier-coordination/recovery/invalid/document.json +34 -0
- package/fixtures/atelier-coordination/recovery/valid/document.json +33 -0
- package/fixtures/atelier-harness-contract/handoff/invalid/document.json +25 -0
- package/fixtures/atelier-harness-contract/handoff/valid/document.json +24 -0
- package/fixtures/atelier-ingestion/invalid/authority.json +19 -0
- package/fixtures/atelier-ingestion/semantic-baseline.json +26 -0
- package/fixtures/atelier-ingestion/semantic-operation.json +75 -0
- package/fixtures/atelier-ingestion/valid/plan-input.json +18 -0
- package/fixtures/atelier-inquiry-contract/assessment/invalid/document.json +41 -0
- package/fixtures/atelier-inquiry-contract/assessment/valid/document.json +40 -0
- package/fixtures/atelier-inquiry-contract/bundle/invalid/document.json +51 -0
- package/fixtures/atelier-inquiry-contract/bundle/valid/document.json +50 -0
- package/fixtures/atelier-inquiry-contract/campaign/invalid/document.json +17 -0
- package/fixtures/atelier-inquiry-contract/campaign/valid/document.json +16 -0
- package/fixtures/atelier-inquiry-contract/decision/invalid/document.json +22 -0
- package/fixtures/atelier-inquiry-contract/decision/valid/document.json +21 -0
- package/fixtures/atelier-inquiry-contract/feedback/invalid/document.json +19 -0
- package/fixtures/atelier-inquiry-contract/feedback/valid/document.json +18 -0
- package/fixtures/atelier-inquiry-contract/handoff/invalid/document.json +92 -0
- package/fixtures/atelier-inquiry-contract/handoff/valid/document.json +91 -0
- package/fixtures/atelier-inquiry-contract/hypothesis/invalid/document.json +20 -0
- package/fixtures/atelier-inquiry-contract/hypothesis/valid/document.json +19 -0
- package/fixtures/atelier-inquiry-contract/ledger/invalid/document.json +680 -0
- package/fixtures/atelier-inquiry-contract/ledger/valid/document.json +679 -0
- package/fixtures/atelier-inquiry-contract/legacy-assessment/invalid/document.json +19 -0
- package/fixtures/atelier-inquiry-contract/legacy-assessment/valid/document.json +18 -0
- package/fixtures/atelier-inquiry-contract/request/invalid/document.json +50 -0
- package/fixtures/atelier-inquiry-contract/request/valid/document.json +49 -0
- package/fixtures/atelier-inquiry-contract/source/invalid/document.json +18 -0
- package/fixtures/atelier-inquiry-contract/source/valid/document.json +17 -0
- package/fixtures/atelier-inquiry-contract/withdrawal/invalid/document.json +16 -0
- package/fixtures/atelier-inquiry-contract/withdrawal/valid/document.json +15 -0
- package/fixtures/atelier-instruction-adoption/context-receipt/invalid/document.json +18 -0
- package/fixtures/atelier-instruction-adoption/context-receipt/valid/document.json +17 -0
- package/fixtures/atelier-instruction-adoption/plan/invalid/document.json +63 -0
- package/fixtures/atelier-instruction-adoption/plan/valid/document.json +62 -0
- package/fixtures/atelier-instruction-adoption/receipt/invalid/document.json +22 -0
- package/fixtures/atelier-instruction-adoption/receipt/valid/document.json +21 -0
- package/fixtures/atelier-instruction-adoption/state/invalid/document.json +30 -0
- package/fixtures/atelier-instruction-adoption/state/valid/document.json +29 -0
- package/fixtures/atelier-interaction/act/invalid/document.json +25 -0
- package/fixtures/atelier-interaction/act/valid/document.json +24 -0
- package/fixtures/atelier-interaction/assessment/invalid/document.json +32 -0
- package/fixtures/atelier-interaction/assessment/valid/document.json +31 -0
- package/fixtures/atelier-interaction/observation/invalid/document.json +22 -0
- package/fixtures/atelier-interaction/observation/valid/document.json +21 -0
- package/fixtures/atelier-interaction/policy/invalid/document.json +20 -0
- package/fixtures/atelier-interaction/policy/valid/document.json +19 -0
- package/fixtures/atelier-judgment/case/invalid/document.json +76 -0
- package/fixtures/atelier-judgment/case/valid/document.json +75 -0
- package/fixtures/atelier-judgment/decision/invalid/document.json +38 -0
- package/fixtures/atelier-judgment/decision/valid/document.json +37 -0
- package/fixtures/atelier-judgment/native-decision.md +3 -0
- package/fixtures/atelier-judgment/practice-reconsideration/definition.json +92 -0
- package/fixtures/atelier-judgment/practice-reconsideration/scenario.json +435 -0
- package/fixtures/atelier-knowledge-contract/activation/invalid/document.json +26 -0
- package/fixtures/atelier-knowledge-contract/activation/valid/document.json +25 -0
- package/fixtures/atelier-knowledge-contract/contribution/invalid/document.json +34 -0
- package/fixtures/atelier-knowledge-contract/contribution/valid/document.json +33 -0
- package/fixtures/atelier-knowledge-contract/domain/invalid/document.json +41 -0
- package/fixtures/atelier-knowledge-contract/domain/valid/document.json +40 -0
- package/fixtures/atelier-knowledge-contract/domain-revision/invalid/document.json +46 -0
- package/fixtures/atelier-knowledge-contract/domain-revision/valid/document.json +45 -0
- package/fixtures/atelier-knowledge-contract/evaluation/invalid/document.json +21 -0
- package/fixtures/atelier-knowledge-contract/evaluation/valid/document.json +20 -0
- package/fixtures/atelier-knowledge-contract/ledger/invalid/document.json +318 -0
- package/fixtures/atelier-knowledge-contract/ledger/valid/document.json +317 -0
- package/fixtures/atelier-knowledge-contract/relation/invalid/document.json +25 -0
- package/fixtures/atelier-knowledge-contract/relation/valid/document.json +24 -0
- package/fixtures/atelier-knowledge-contract/review/invalid/document.json +18 -0
- package/fixtures/atelier-knowledge-contract/review/valid/document.json +17 -0
- package/fixtures/atelier-knowledge-contract/withdrawal/invalid/document.json +16 -0
- package/fixtures/atelier-knowledge-contract/withdrawal/valid/document.json +15 -0
- package/fixtures/atelier-learning/invalid/authority.json +14 -0
- package/fixtures/atelier-learning/valid/capture.json +13 -0
- package/fixtures/atelier-personal-workspace-contract/manifest/invalid/authority-key.json +17 -0
- package/fixtures/atelier-personal-workspace-contract/manifest/invalid/nonempty-ext.json +25 -0
- package/fixtures/atelier-personal-workspace-contract/manifest/invalid/wrong-schema.json +22 -0
- package/fixtures/atelier-personal-workspace-contract/manifest/valid/manifest.json +22 -0
- package/fixtures/atelier-personal-workspace-contract/overlay/invalid/authority-key.json +25 -0
- package/fixtures/atelier-personal-workspace-contract/overlay/invalid/unknown-top-level-field.json +27 -0
- package/fixtures/atelier-personal-workspace-contract/overlay/invalid/wrong-schema.json +24 -0
- package/fixtures/atelier-trackable/definition/invalid/document.json +15 -0
- package/fixtures/atelier-trackable/definition/valid/document.json +14 -0
- package/fixtures/atelier-trackable/request/invalid/document.json +23 -0
- package/fixtures/atelier-trackable/request/valid/document.json +22 -0
- package/fixtures/boundary-delegations/invalid/authority-key.v1.json +30 -0
- package/fixtures/boundary-delegations/invalid/push-operation.v1.json +28 -0
- package/fixtures/boundary-delegations/invalid/unsigned.v1.json +25 -0
- package/fixtures/boundary-delegations/valid/empty.v1.json +4 -0
- package/fixtures/boundary-delegations/valid/signed-operator.v1.json +30 -0
- package/fixtures/boundary-owner-keys/invalid/wrong-algorithm.v1.json +15 -0
- package/fixtures/boundary-owner-keys/invalid/wrong-curve.v1.json +15 -0
- package/fixtures/boundary-owner-keys/valid/one-owner.v1.json +17 -0
- package/fixtures/capability-packages/adoption.example.json +70 -0
- package/fixtures/capability-packages/evidence-review/capability-package.json +58 -0
- package/fixtures/capability-packages/evidence-review/capability-release.json +84 -0
- package/fixtures/capability-packages/evidence-review/evaluation.md +15 -0
- package/fixtures/capability-packages/evidence-review/skills/evidence-review/SKILL.md +22 -0
- package/fixtures/capability-packages/evidence-review/skills/evidence-review/references/ledger.md +7 -0
- package/fixtures/capability-packages/research/capability-package.json +88 -0
- package/fixtures/capability-packages/research/capability-release.json +134 -0
- package/fixtures/capability-packages/research/evaluation.md +15 -0
- package/fixtures/capability-packages/research/skills/research-integrate/SKILL.md +29 -0
- package/fixtures/capability-packages/research/skills/research-integrate/references/integration.md +10 -0
- package/fixtures/capability-packages/research/skills/research-plan/SKILL.md +27 -0
- package/fixtures/capability-packages/research/skills/research-plan/references/inquiries.md +26 -0
- package/fixtures/capability-packages/research/skills/research-synthesize/SKILL.md +23 -0
- package/fixtures/capability-packages/research/skills/research-synthesize/references/synthesis.md +12 -0
- package/fixtures/evidence-navigation/invalid/duplicate-operation.json +16 -0
- package/fixtures/evidence-navigation/invalid/instruction-kind.json +36 -0
- package/fixtures/evidence-navigation/invalid/invalid-digest.json +36 -0
- package/fixtures/evidence-navigation/invalid/invalid-time.json +36 -0
- package/fixtures/evidence-navigation/invalid/missing-attribution.json +35 -0
- package/fixtures/evidence-navigation/invalid/reserved-extension.json +19 -0
- package/fixtures/evidence-navigation/invalid/unattributed-acceptance.json +36 -0
- package/fixtures/evidence-navigation/invalid/uncoerced-version.json +16 -0
- package/fixtures/evidence-navigation/invalid/unknown-currency.json +17 -0
- package/fixtures/evidence-navigation/invalid/unknown-selector-field.json +37 -0
- package/fixtures/evidence-navigation/invalid/unsupported-operation.json +15 -0
- package/fixtures/evidence-navigation/valid/claim.json +36 -0
- package/fixtures/evidence-navigation/valid/host.json +16 -0
- package/fixtures/evidence-navigation/valid/profile.json +16 -0
- package/fixtures/evidence-navigation/valid/snapshot.json +17 -0
- package/fixtures/harness-packages/build-harness/capability-package.json +61 -0
- package/fixtures/harness-packages/build-harness/capability-release.json +82 -0
- package/fixtures/harness-packages/build-harness/evaluation.md +3 -0
- package/fixtures/harness-packages/build-harness/skills/atelier-build-harness/SKILL.md +59 -0
- package/fixtures/harness-packages/knowledge-harness/capability-package.json +61 -0
- package/fixtures/harness-packages/knowledge-harness/capability-release.json +82 -0
- package/fixtures/harness-packages/knowledge-harness/evaluation.md +3 -0
- package/fixtures/harness-packages/knowledge-harness/skills/atelier-knowledge-harness/SKILL.md +60 -0
- package/fixtures/harnesses/learning-cycle.json +780 -0
- package/fixtures/inquiry/workshop.json +675 -0
- package/fixtures/inquiry-packages/discovery-harness/capability-package.json +61 -0
- package/fixtures/inquiry-packages/discovery-harness/capability-release.json +82 -0
- package/fixtures/inquiry-packages/discovery-harness/evaluation.md +3 -0
- package/fixtures/inquiry-packages/discovery-harness/skills/atelier-discovery-harness/SKILL.md +48 -0
- package/fixtures/inquiry-packages/research-harness/capability-package.json +61 -0
- package/fixtures/inquiry-packages/research-harness/capability-release.json +82 -0
- package/fixtures/inquiry-packages/research-harness/evaluation.md +3 -0
- package/fixtures/inquiry-packages/research-harness/skills/atelier-research-harness/SKILL.md +43 -0
- package/fixtures/knowledge-health/calibration-suite.json +26 -0
- package/fixtures/knowledge-health/calibration-trial.json +37 -0
- package/fixtures/knowledge-health/corrected/checklist.md +18 -0
- package/fixtures/knowledge-health/devday/participant-record.json +19 -0
- package/fixtures/knowledge-health/expected-summary.json +27 -0
- package/fixtures/knowledge-health/workshop/README.md +20 -0
- package/fixtures/knowledge-health/workshop/atelier.project.json +7 -0
- package/fixtures/knowledge-health/workshop/knowledge-plan.json +65 -0
- package/fixtures/knowledge-health/workshop/records/checklist.md +18 -0
- package/fixtures/knowledge-health/workshop/records/workshop.md +18 -0
- package/fixtures/knowledge-health/workshop/repo-access.v1.json +5 -0
- package/fixtures/knowledge-health/workspace/atelier.project.json +18 -0
- package/fixtures/knowledge-health/workspace/knowledge-plan.json +74 -0
- package/fixtures/knowledge-health/workspace/records/checklist.md +18 -0
- package/fixtures/knowledge-health/workspace/records/pilot.md +16 -0
- package/fixtures/knowledge-health/workspace/repo-access.v1.json +9 -0
- package/fixtures/personal-workspace/overlay.json +24 -0
- package/fixtures/templates/local-library.v1.json +76 -0
- package/fixtures/templates/upgrade-contracts/adoption-policy.v2/invalid.json +16 -0
- package/fixtures/templates/upgrade-contracts/adoption-policy.v2/valid.json +13 -0
- package/fixtures/templates/upgrade-contracts/migration.v3/invalid.json +24 -0
- package/fixtures/templates/upgrade-contracts/migration.v3/valid.json +21 -0
- package/fixtures/templates/upgrade-contracts/template-adoption.v1/invalid.json +60 -0
- package/fixtures/templates/upgrade-contracts/template-adoption.v1/valid.json +57 -0
- package/fixtures/templates/upgrade-contracts/upgrade-plan.v3/invalid.json +85 -0
- package/fixtures/templates/upgrade-contracts/upgrade-plan.v3/valid.json +82 -0
- package/package.json +60 -5
- package/plugins/obsidian/main.js +57 -8
- package/plugins/obsidian/manifest.json +1 -1
- package/skills/claude/atelier-build-harness/SKILL.md +59 -0
- package/skills/claude/atelier-coordination/SKILL.md +32 -0
- package/skills/claude/atelier-discovery-harness/SKILL.md +48 -0
- package/skills/claude/atelier-guided-upgrade/SKILL.md +29 -0
- package/skills/claude/atelier-interaction/SKILL.md +34 -0
- package/skills/claude/atelier-knowledge-harness/SKILL.md +60 -0
- package/skills/claude/atelier-knowledge-setup/SKILL.md +72 -0
- package/skills/claude/atelier-practical-judgment/SKILL.md +64 -0
- package/skills/claude/atelier-research-harness/SKILL.md +43 -0
- package/skills/claude/atelier-skill-steward/SKILL.md +53 -0
- package/skills/claude/atelier-trackables/SKILL.md +40 -0
- package/skills/codex/atelier-build-harness/SKILL.md +59 -0
- package/skills/codex/atelier-coordination/SKILL.md +32 -0
- package/skills/codex/atelier-discovery-harness/SKILL.md +48 -0
- package/skills/codex/atelier-guided-upgrade/SKILL.md +29 -0
- package/skills/codex/atelier-interaction/SKILL.md +34 -0
- package/skills/codex/atelier-knowledge-harness/SKILL.md +60 -0
- package/skills/codex/atelier-knowledge-setup/SKILL.md +72 -0
- package/skills/codex/atelier-practical-judgment/SKILL.md +64 -0
- package/skills/codex/atelier-research-harness/SKILL.md +43 -0
- package/skills/codex/atelier-skill-steward/SKILL.md +53 -0
- package/skills/codex/atelier-trackables/SKILL.md +40 -0
- package/src/architecture/catalog.mjs +355 -0
- package/src/architecture/index.mjs +93 -0
- package/src/boundary/content-rules.mjs +536 -69
- package/src/boundary/delegation-command.mjs +158 -0
- package/src/boundary/delegation.mjs +396 -0
- package/src/boundary/policy.mjs +185 -64
- package/src/build/git.mjs +18 -0
- package/src/build/index.mjs +3 -0
- package/src/build/ledger.mjs +88 -0
- package/src/capabilities/adoption.mjs +381 -0
- package/src/capabilities/evidence.mjs +140 -0
- package/src/capabilities/files.mjs +124 -0
- package/src/capabilities/index.mjs +4 -0
- package/src/capabilities/instructions.mjs +250 -0
- package/src/capabilities/package.mjs +136 -0
- package/src/cli/command-failure.mjs +36 -0
- package/src/cli/execute-command.mjs +16 -2
- package/src/cli/run.mjs +66 -9
- package/src/coauthor/store.mjs +43 -2
- package/src/collaboration/proposals.mjs +189 -0
- package/src/commands/architecture.mjs +27 -0
- package/src/commands/boundary.mjs +3 -0
- package/src/commands/capability.mjs +73 -0
- package/src/commands/enroll.mjs +3 -0
- package/src/commands/harness.mjs +79 -0
- package/src/commands/ingest.mjs +39 -0
- package/src/commands/init.mjs +4 -1
- package/src/commands/inquiry.mjs +66 -0
- package/src/commands/knowledge-semantic.mjs +128 -0
- package/src/commands/knowledge.mjs +119 -0
- package/src/commands/learn.mjs +61 -0
- package/src/commands/obsidian.mjs +772 -65
- package/src/commands/practice.mjs +24 -0
- package/src/commands/setup.mjs +22 -2
- package/src/commands/trackable.mjs +19 -0
- package/src/contracts/corpus.mjs +30 -0
- package/src/coordination/index.mjs +110 -0
- package/src/evidence-navigation/contracts.mjs +60 -0
- package/src/evidence-navigation/evaluate.mjs +69 -0
- package/src/evidence-navigation/generation.mjs +22 -0
- package/src/evidence-navigation/index.mjs +2 -0
- package/src/evidence-navigation/local.mjs +167 -0
- package/src/evidence-navigation/schema.generated.mjs +604 -0
- package/src/evidence-navigation/types.mjs +12 -0
- package/src/export/atelier-export-contract.mjs +35 -8
- package/src/graph/enroll.mjs +248 -0
- package/src/graph/graph.mjs +30 -6
- package/src/graph/knowledge-graph.mjs +77 -9
- package/src/harnesses/contracts.mjs +41 -0
- package/src/harnesses/exchange.mjs +154 -0
- package/src/harnesses/feedback.mjs +16 -0
- package/src/harnesses/history.mjs +37 -0
- package/src/harnesses/index.mjs +4 -0
- package/src/harnesses/store.mjs +29 -0
- package/src/ingestion/contracts.mjs +41 -0
- package/src/ingestion/evaluation.mjs +119 -0
- package/src/ingestion/optional/composition/index.mjs +171 -0
- package/src/ingestion/optional/evaluation/index.mjs +219 -0
- package/src/ingestion/optional/graphify/composition-binding.mjs +60 -0
- package/src/ingestion/optional/graphify/composition_receiving.py +185 -0
- package/src/ingestion/optional/graphify/graphify_supplier.py +938 -0
- package/src/ingestion/optional/graphify/json_bridge.py +163 -0
- package/src/ingestion/optional/graphify/native-closure-pins.json +275 -0
- package/src/ingestion/optional/graphify/native-pins.json +306 -0
- package/src/ingestion/optional/graphify/query_receiving.py +209 -0
- package/src/ingestion/optional/index.mjs +9 -0
- package/src/ingestion/optional/jev/bridge.mjs +166 -0
- package/src/ingestion/optional/jev/composition-binding.mjs +116 -0
- package/src/ingestion/optional/jev/judgments.mjs +174 -0
- package/src/ingestion/optional/jev/native-translation.mjs +32 -0
- package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/__init__.py +7 -0
- package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/adapter.py +966 -0
- package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/artifacts.py +101 -0
- package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/projection.py +146 -0
- package/src/ingestion/optional/lightrag/composition-binding.mjs +67 -0
- package/src/ingestion/optional/vanilla/composition-binding.mjs +102 -0
- package/src/ingestion/optional/vanilla/index.mjs +218 -0
- package/src/ingestion/optional/vanilla/native-normalization.mjs +187 -0
- package/src/ingestion/processors.mjs +218 -0
- package/src/ingestion/semantic.mjs +203 -0
- package/src/ingestion/store.mjs +372 -0
- package/src/inquiry/beliefs.mjs +32 -0
- package/src/inquiry/contracts.mjs +23 -0
- package/src/inquiry/index.mjs +5 -0
- package/src/inquiry/ledger.mjs +135 -0
- package/src/inquiry/lenses.mjs +15 -0
- package/src/inquiry/projection.mjs +77 -0
- package/src/inquiry/store.mjs +35 -0
- package/src/intake/read-scope.mjs +12 -0
- package/src/intake/store.mjs +69 -6
- package/src/interaction/index.mjs +91 -0
- package/src/judgment/index.mjs +82 -0
- package/src/judgment/practice-evaluation.mjs +70 -0
- package/src/judgment/practice.mjs +144 -0
- package/src/knowledge/context.mjs +217 -0
- package/src/knowledge/index.mjs +5 -0
- package/src/knowledge/ingestion.mjs +79 -0
- package/src/knowledge/intake.mjs +33 -0
- package/src/knowledge/ledger.mjs +110 -0
- package/src/knowledge/participatory/guidance.mjs +36 -0
- package/src/knowledge/participatory/guided-contribution.mjs +44 -0
- package/src/knowledge/participatory/index.mjs +2 -0
- package/src/knowledge/participatory/installed-workshop.mjs +108 -0
- package/src/knowledge/plan.mjs +67 -0
- package/src/knowledge/projection.mjs +43 -0
- package/src/knowledge/semantic-operation.mjs +567 -0
- package/src/knowledge/sessions.mjs +467 -0
- package/src/knowledge/workspace.mjs +295 -0
- package/src/knowledge-health/assessment.mjs +165 -0
- package/src/knowledge-health/capabilities.mjs +350 -0
- package/src/knowledge-health/context.mjs +62 -0
- package/src/knowledge-health/example.mjs +47 -0
- package/src/knowledge-health/index.mjs +10 -0
- package/src/knowledge-health/owner-join.mjs +88 -0
- package/src/knowledge-health/proof/common.mjs +24 -0
- package/src/knowledge-health/proof/installed-check.mjs +100 -0
- package/src/knowledge-health/proof/native-readback.mjs +37 -0
- package/src/knowledge-health/proof/rehearse.mjs +68 -0
- package/src/knowledge-health/proof/validate-participant.mjs +17 -0
- package/src/knowledge-health/proof/verify-native-capture.mjs +14 -0
- package/src/knowledge-health/proof/workshop-check.mjs +36 -0
- package/src/knowledge-health/rule-identity.mjs +49 -0
- package/src/knowledge-health/workshop-composition.mjs +19 -0
- package/src/knowledge-health/workshop-profile.mjs +176 -0
- package/src/knowledge-health/workshop.mjs +99 -0
- package/src/learning/adapters.mjs +68 -0
- package/src/learning/assistance.mjs +127 -0
- package/src/learning/contracts.mjs +65 -0
- package/src/learning/store.mjs +272 -0
- package/src/personal-workspace/index.mjs +391 -0
- package/src/personal-workspace/selection.mjs +401 -0
- package/src/project/config.mjs +8 -0
- package/src/project/durable-state.mjs +3 -1
- package/src/project/file-class.mjs +1 -1
- package/src/project/path-match.mjs +2 -1
- package/src/projection/obsidian/edits/apply.mjs +55 -16
- package/src/projection/obsidian/materialize/prepare-view.mjs +87 -5
- package/src/projection/obsidian/personal-workspace.mjs +519 -0
- package/src/projection/obsidian/proposals/adapter.mjs +79 -17
- package/src/projection/obsidian/proposals/observation.mjs +50 -4
- package/src/projection/obsidian/publication/publisher.mjs +214 -18
- package/src/projection/obsidian/publication/vault-list.mjs +4 -3
- package/src/projection/obsidian/recovery/index.mjs +4 -1
- package/src/projection/obsidian/recovery/store.mjs +160 -7
- package/src/projection/obsidian/selection-ui/contribution.mjs +6 -5
- package/src/projection/project.mjs +3 -2
- package/src/readiness/readiness.mjs +10 -3
- package/src/reflection/index.mjs +56 -0
- package/src/runtime/obsidian/app-production-seams.mjs +48 -16
- package/src/runtime/obsidian/app-registration.mjs +175 -28
- package/src/runtime/obsidian/app-restart.mjs +154 -0
- package/src/runtime/obsidian/engine.mjs +437 -56
- package/src/runtime/obsidian/extension-points.mjs +1 -1
- package/src/runtime/obsidian/index.mjs +10 -7
- package/src/runtime/obsidian/launch-plan.mjs +7 -4
- package/src/runtime/obsidian/lifecycle.mjs +108 -23
- package/src/runtime/obsidian/login-item.mjs +335 -0
- package/src/runtime/obsidian/machine-settings.mjs +119 -12
- package/src/runtime/obsidian/observation.mjs +36 -4
- package/src/runtime/obsidian/opening.mjs +173 -38
- package/src/runtime/obsidian/personal-selection.mjs +125 -0
- package/src/runtime/obsidian/pipeline.mjs +115 -6
- package/src/runtime/obsidian/plugin-channel.mjs +11 -3
- package/src/runtime/obsidian/plugin-choice.mjs +16 -4
- package/src/runtime/obsidian/plugin-drift.mjs +59 -11
- package/src/runtime/obsidian/plugin-presence.mjs +14 -1
- package/src/runtime/obsidian/private-lock.mjs +39 -4
- package/src/runtime/obsidian/process-identity.mjs +20 -10
- package/src/runtime/obsidian/project-views.mjs +282 -0
- package/src/runtime/obsidian/questions.mjs +48 -0
- package/src/runtime/obsidian/release-watch.mjs +56 -0
- package/src/runtime/obsidian/service-main.mjs +148 -19
- package/src/runtime/obsidian/service-manager-production.mjs +61 -0
- package/src/runtime/obsidian/service-managers.mjs +275 -0
- package/src/runtime/obsidian/service-record.mjs +87 -23
- package/src/runtime/obsidian/service.mjs +102 -9
- package/src/runtime/obsidian/startup-adapters.mjs +59 -21
- package/src/runtime/obsidian/unheld-evidence.mjs +84 -0
- package/src/runtime/obsidian/vault-location.mjs +300 -0
- package/src/runtime/obsidian/view-counts.mjs +34 -0
- package/src/runtime/obsidian/view-permission.mjs +334 -0
- package/src/server/local-sidecar.mjs +169 -2
- package/src/server/server.mjs +28 -2
- package/src/template-bindings/project.mjs +92 -0
- package/src/templates/profile.mjs +225 -0
- package/src/trackables/domain.mjs +175 -0
- package/src/trackables/store.mjs +114 -0
- package/src/ui/control-system.mjs +201 -0
- package/src/ui/html-primitives.mjs +3 -0
- package/src/ui/knowledge-page.mjs +1388 -0
- package/src/upgrade/closure-diagnostics.mjs +135 -0
- package/src/upgrade/closure.mjs +649 -0
- package/src/upgrade/explanation.mjs +1 -1
- package/src/upgrade/template-participant.mjs +171 -0
- package/src/upgrade/transaction.mjs +72 -25
- package/src/upgrade/upgrade.mjs +20 -7
- package/templates/external-project-workspace/pack-lifecycle.json +7 -1
- package/templates/knowledge-workspace/.gitattributes +2 -0
- package/templates/knowledge-workspace/AGENTS.md +27 -0
- package/templates/knowledge-workspace/README.md +44 -0
- package/templates/knowledge-workspace/atelier.project.json +22 -0
- package/templates/knowledge-workspace/gitignore +6 -0
- package/templates/knowledge-workspace/knowledge-plan.json +111 -0
- package/templates/knowledge-workspace/knowledge-plan.schema.json +281 -0
- package/templates/knowledge-workspace/records/forecast.md +22 -0
- package/templates/knowledge-workspace/records/inspection.md +25 -0
- package/templates/knowledge-workspace/records/telescope.md +24 -0
- package/templates/knowledge-workspace/repo-access.v1.json +9 -0
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
2
3
|
import fs from 'node:fs'
|
|
3
4
|
import path from 'node:path'
|
|
4
5
|
import { matchesPathPattern, normalizeRelPath } from '../project/path-match.mjs'
|
|
5
6
|
import { sanitizedGitEnvironment } from '../runtime/git-adapter.mjs'
|
|
6
7
|
|
|
7
8
|
export const EMPTY_TREE = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'
|
|
9
|
+
const EMPTY_TREES = Object.freeze({ sha1: EMPTY_TREE, sha256: '6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321' })
|
|
10
|
+
// Commits one push may carry before the per-commit scan refuses rather than run long.
|
|
11
|
+
export const PUSH_COMMIT_SCAN_MAX = 1000
|
|
8
12
|
export const ZERO_SHA_RE = /^0{40,}$/
|
|
9
13
|
|
|
10
14
|
export const RULE_KINDS = Object.freeze(['content', 'path'])
|
|
@@ -62,8 +66,9 @@ export function validateContentRules(rules, { label = 'contentRules' } = {}) {
|
|
|
62
66
|
continue
|
|
63
67
|
}
|
|
64
68
|
for (const key of Object.keys(rule)) {
|
|
65
|
-
if (!['id', 'kind', 'severity', 'pattern', 'paths', 'description'].includes(key)) errors.push(`${at} must not include additional property ${key}`)
|
|
69
|
+
if (!['id', 'kind', 'severity', 'pattern', 'paths', 'description', 'ext'].includes(key)) errors.push(`${at} must not include additional property ${key}`)
|
|
66
70
|
}
|
|
71
|
+
if (rule.ext != null && !isObject(rule.ext)) errors.push(`${at}.ext must be an object`)
|
|
67
72
|
const id = trimmed(rule.id)
|
|
68
73
|
if (!id) errors.push(`${at}.id is required`)
|
|
69
74
|
else if (seen.has(id)) errors.push(`${at}.id duplicates an earlier rule: ${id}`)
|
|
@@ -96,8 +101,9 @@ export function validateContentRuleExceptions(exceptions, rules = DEFAULT_CONTEN
|
|
|
96
101
|
continue
|
|
97
102
|
}
|
|
98
103
|
for (const key of Object.keys(exception)) {
|
|
99
|
-
if (!['rule', 'repo', 'paths', 'reason'].includes(key)) errors.push(`${at} must not include additional property ${key}`)
|
|
104
|
+
if (!['rule', 'repo', 'paths', 'reason', 'ext'].includes(key)) errors.push(`${at} must not include additional property ${key}`)
|
|
100
105
|
}
|
|
106
|
+
if (exception.ext != null && !isObject(exception.ext)) errors.push(`${at}.ext must be an object`)
|
|
101
107
|
const rule = trimmed(exception.rule)
|
|
102
108
|
if (!rule) errors.push(`${at}.rule is required; an exception must name the rule it excepts`)
|
|
103
109
|
else if (ruleIds.size && !ruleIds.has(rule)) errors.push(`${at}.rule "${rule}" is not a declared content rule`)
|
|
@@ -126,6 +132,9 @@ export function resolveContentRules(policy) {
|
|
|
126
132
|
}
|
|
127
133
|
|
|
128
134
|
export function findException({ exceptions = [], rule, repo, path: filePath }) {
|
|
135
|
+
// Exception paths are slash-normalised patterns, so they cannot name a file whose
|
|
136
|
+
// own name holds a backslash; such a file never inherits a sibling's exception.
|
|
137
|
+
if (String(filePath ?? '').includes('\\')) return null
|
|
129
138
|
const rel = normalizeRelPath(filePath)
|
|
130
139
|
return (
|
|
131
140
|
exceptions.find(
|
|
@@ -139,6 +148,8 @@ export function findException({ exceptions = [], rule, repo, path: filePath }) {
|
|
|
139
148
|
|
|
140
149
|
function rulePathMatches(rule, filePath) {
|
|
141
150
|
if (!Array.isArray(rule.paths) || !rule.paths.length) return true
|
|
151
|
+
// Path scopes are slash-normalised too; a backslash name is treated as in scope.
|
|
152
|
+
if (String(filePath ?? '').includes('\\')) return true
|
|
142
153
|
return rule.paths.some((pattern) => matchesPathPattern(pattern, filePath))
|
|
143
154
|
}
|
|
144
155
|
|
|
@@ -152,13 +163,15 @@ function rulePathMatches(rule, filePath) {
|
|
|
152
163
|
export function scanAddedContent({ files, rules = DEFAULT_CONTENT_RULES, exceptions = [], repo = null }) {
|
|
153
164
|
const findings = []
|
|
154
165
|
for (const file of files) {
|
|
155
|
-
|
|
166
|
+
// Paths from Git are exact repository paths and are reported as they are.
|
|
167
|
+
const filePath = String(file.path ?? '')
|
|
156
168
|
for (const rule of rules) {
|
|
157
169
|
const kind = rule.kind ?? 'content'
|
|
158
170
|
const severity = rule.severity ?? 'error'
|
|
159
171
|
const exception = findException({ exceptions, rule: rule.id, repo, path: filePath })
|
|
160
172
|
if (kind === 'path') {
|
|
161
|
-
|
|
173
|
+
const pathRule = new RegExp(rule.pattern, 'i')
|
|
174
|
+
if (!file.added || !(pathRule.test(filePath) || pathRule.test(normalizeRelPath(filePath)))) continue
|
|
162
175
|
if (exception) continue
|
|
163
176
|
findings.push({ severity, code: 'content-rule-violation', rule: rule.id, repo, path: filePath, line: null, message: `${repo ?? ''}/${filePath}: ${rule.description ?? rule.id}`.replace(/^\//, '') })
|
|
164
177
|
continue
|
|
@@ -168,7 +181,7 @@ export function scanAddedContent({ files, rules = DEFAULT_CONTENT_RULES, excepti
|
|
|
168
181
|
for (const line of file.addedLines ?? []) {
|
|
169
182
|
if (!re.test(line.text)) continue
|
|
170
183
|
if (exception) continue
|
|
171
|
-
|
|
184
|
+
const found = {
|
|
172
185
|
severity,
|
|
173
186
|
code: 'content-rule-violation',
|
|
174
187
|
rule: rule.id,
|
|
@@ -176,45 +189,242 @@ export function scanAddedContent({ files, rules = DEFAULT_CONTENT_RULES, excepti
|
|
|
176
189
|
path: filePath,
|
|
177
190
|
line: line.number,
|
|
178
191
|
message: `${repo ?? ''}/${filePath}:${line.number}: ${rule.description ?? rule.id}`.replace(/^\//, ''),
|
|
179
|
-
}
|
|
192
|
+
}
|
|
193
|
+
// A digest of the line identifies the same content across diffs; it is
|
|
194
|
+
// not enumerable, so it never appears in a report.
|
|
195
|
+
Object.defineProperty(found, 'lineDigest', { value: createHash('sha256').update(line.text).digest('hex') })
|
|
196
|
+
findings.push(found)
|
|
180
197
|
}
|
|
181
198
|
}
|
|
182
199
|
}
|
|
183
200
|
return findings
|
|
184
201
|
}
|
|
185
202
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
203
|
+
// The diff commands below pin their own output format, so repository or user
|
|
204
|
+
// configuration (prefixes, colour, external diff drivers, textconv, relative
|
|
205
|
+
// paths, fused hunks, submodule summaries) cannot change what the parser reads.
|
|
206
|
+
// Anything it still cannot read is reported and blocks, so a key not pinned here
|
|
207
|
+
// can stop a commit but cannot hide content.
|
|
208
|
+
export const CANONICAL_DIFF_FLAGS = Object.freeze([
|
|
209
|
+
'--no-ext-diff',
|
|
210
|
+
'--no-textconv',
|
|
211
|
+
'--no-color',
|
|
212
|
+
'--src-prefix=a/',
|
|
213
|
+
'--dst-prefix=b/',
|
|
214
|
+
'--no-relative',
|
|
215
|
+
'--inter-hunk-context=0',
|
|
216
|
+
'--submodule=short',
|
|
217
|
+
'--full-index',
|
|
218
|
+
])
|
|
219
|
+
|
|
220
|
+
const OCTAL_ESCAPE = /^[0-7]{3}$/
|
|
221
|
+
const SIMPLE_ESCAPES = { a: 7, b: 8, t: 9, n: 10, v: 11, f: 12, r: 13, '"': 34, '\\': 92 }
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Read one C-quoted path token as Git writes it (core.quotePath), starting at the
|
|
225
|
+
* opening quote. Returns the decoded path and the index after the closing quote,
|
|
226
|
+
* or null when the token is malformed.
|
|
227
|
+
*/
|
|
228
|
+
function readQuotedToken(text, start) {
|
|
229
|
+
if (text[start] !== '"') return null
|
|
230
|
+
const bytes = []
|
|
231
|
+
for (let index = start + 1; index < text.length; index += 1) {
|
|
232
|
+
const char = String.fromCodePoint(text.codePointAt(index))
|
|
233
|
+
if (char === '"') return { value: Buffer.from(bytes).toString('utf8'), end: index + 1 }
|
|
234
|
+
if (char !== '\\') {
|
|
235
|
+
bytes.push(...Buffer.from(char, 'utf8'))
|
|
236
|
+
index += char.length - 1
|
|
237
|
+
continue
|
|
238
|
+
}
|
|
239
|
+
const next = text[index + 1]
|
|
240
|
+
if (next === undefined) return null
|
|
241
|
+
const octal = text.slice(index + 1, index + 4)
|
|
242
|
+
if (OCTAL_ESCAPE.test(octal)) {
|
|
243
|
+
const byte = Number.parseInt(octal, 8)
|
|
244
|
+
if (byte > 255) return null
|
|
245
|
+
bytes.push(byte)
|
|
246
|
+
index += 3
|
|
247
|
+
} else if (Object.hasOwn(SIMPLE_ESCAPES, next)) {
|
|
248
|
+
bytes.push(SIMPLE_ESCAPES[next])
|
|
249
|
+
index += 1
|
|
250
|
+
} else {
|
|
251
|
+
return null
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return null
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** A path as it appears after a fixed prefix in a header line, quoted or bare. */
|
|
258
|
+
function prefixedPath(token, prefix) {
|
|
259
|
+
if (token.startsWith('"')) {
|
|
260
|
+
const quoted = readQuotedToken(token, 0)
|
|
261
|
+
if (!quoted || quoted.end !== token.length || !quoted.value.startsWith(prefix)) return null
|
|
262
|
+
return quoted.value.slice(prefix.length)
|
|
263
|
+
}
|
|
264
|
+
return token.startsWith(prefix) ? token.slice(prefix.length) : null
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* The destination path of a `diff --git` header. A bare header is ambiguous when
|
|
269
|
+
* a path contains " b/", so it is resolved only when both sides name the same
|
|
270
|
+
* path, which is every case except a rename or copy; those are resolved from the
|
|
271
|
+
* `rename to` / `copy to` lines that follow.
|
|
272
|
+
*/
|
|
273
|
+
export function diffHeaderPath(line) {
|
|
274
|
+
if (!line.startsWith('diff --git ')) return null
|
|
275
|
+
const rest = line.slice('diff --git '.length)
|
|
276
|
+
if (rest.startsWith('"')) {
|
|
277
|
+
const first = readQuotedToken(rest, 0)
|
|
278
|
+
if (!first || rest[first.end] !== ' ') return null
|
|
279
|
+
return prefixedPath(rest.slice(first.end + 1), 'b/')
|
|
280
|
+
}
|
|
281
|
+
if (rest.endsWith('"')) {
|
|
282
|
+
const open = rest.lastIndexOf(' "')
|
|
283
|
+
return open < 0 ? null : prefixedPath(rest.slice(open + 1), 'b/')
|
|
284
|
+
}
|
|
285
|
+
if (rest.length < 5 || (rest.length - 5) % 2 !== 0) return null
|
|
286
|
+
const half = (rest.length - 5) / 2
|
|
287
|
+
const candidate = rest.slice(2, 2 + half)
|
|
288
|
+
return half > 0 && rest === `a/${candidate} b/${candidate}` ? candidate : null
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const HUNK_HEADER = /^@@ -\d+(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/
|
|
292
|
+
const INDEX_LINE = /^index ([0-9a-f]{40,64})\.\.([0-9a-f]{40,64})(?: \d+)?$/
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Split `git diff --unified=0` output into file sections. Lines are read as
|
|
296
|
+
* content only inside a hunk, for exactly as many lines as its header counts, so
|
|
297
|
+
* content can never be read as a header and a header can never hide content. A
|
|
298
|
+
* section whose path or hunks cannot be read is returned with `incomplete` set,
|
|
299
|
+
* so the caller fails closed instead of skipping it. Paths are the exact
|
|
300
|
+
* repository paths Git reports; `newBlob` is the object id of the new content.
|
|
301
|
+
* With `detail`, each section also keeps its hunks (added and removed text) and
|
|
302
|
+
* the raw lines after its header.
|
|
303
|
+
*/
|
|
304
|
+
export function parseDiffSections(diff, { detail = false } = {}) {
|
|
305
|
+
const sections = []
|
|
189
306
|
let current = null
|
|
307
|
+
let pendingAdded = 0
|
|
308
|
+
let pendingRemoved = 0
|
|
190
309
|
let lineNumber = 0
|
|
191
310
|
for (const raw of String(diff ?? '').split('\n')) {
|
|
311
|
+
if (detail && current && !raw.startsWith('diff --git ') && !raw.startsWith('* Unmerged path ')) current.lines.push(raw)
|
|
312
|
+
if (pendingAdded > 0 || pendingRemoved > 0) {
|
|
313
|
+
if (raw.startsWith('+') && pendingAdded > 0) {
|
|
314
|
+
current.addedLines.push({ number: lineNumber, text: raw.slice(1) })
|
|
315
|
+
if (detail) current.hunks.at(-1).added.push(raw.slice(1))
|
|
316
|
+
lineNumber += 1
|
|
317
|
+
pendingAdded -= 1
|
|
318
|
+
continue
|
|
319
|
+
}
|
|
320
|
+
if (raw.startsWith('-') && pendingRemoved > 0) {
|
|
321
|
+
if (detail) current.hunks.at(-1).removed.push(raw.slice(1))
|
|
322
|
+
pendingRemoved -= 1
|
|
323
|
+
continue
|
|
324
|
+
}
|
|
325
|
+
if (raw.startsWith(' ') && pendingAdded > 0 && pendingRemoved > 0) {
|
|
326
|
+
// A context line counts on both sides; none is expected, but one is harmless.
|
|
327
|
+
lineNumber += 1
|
|
328
|
+
pendingAdded -= 1
|
|
329
|
+
pendingRemoved -= 1
|
|
330
|
+
continue
|
|
331
|
+
}
|
|
332
|
+
if (raw.startsWith('\\')) continue // ""
|
|
333
|
+
current.incomplete = 'hunk-shorter-than-its-header'
|
|
334
|
+
pendingAdded = 0
|
|
335
|
+
pendingRemoved = 0
|
|
336
|
+
}
|
|
192
337
|
if (raw.startsWith('diff --git ')) {
|
|
193
|
-
const
|
|
194
|
-
current = {
|
|
195
|
-
|
|
338
|
+
const headerPath = diffHeaderPath(raw)
|
|
339
|
+
current = {
|
|
340
|
+
path: headerPath === null || headerPath === '' ? null : headerPath,
|
|
341
|
+
added: false,
|
|
342
|
+
deleted: false,
|
|
343
|
+
binary: false,
|
|
344
|
+
newBlob: null,
|
|
345
|
+
oldPath: null,
|
|
346
|
+
addedLines: [],
|
|
347
|
+
incomplete: null,
|
|
348
|
+
inHeader: true,
|
|
349
|
+
...(detail ? { hunks: [], lines: [] } : {}),
|
|
350
|
+
}
|
|
351
|
+
sections.push(current)
|
|
196
352
|
continue
|
|
197
353
|
}
|
|
198
|
-
if (
|
|
199
|
-
|
|
200
|
-
|
|
354
|
+
if (raw.startsWith('* Unmerged path ')) {
|
|
355
|
+
// An unmerged entry has no patch to read; it stands as its own unreadable
|
|
356
|
+
// section instead of being taken for a line of the file before it. Git
|
|
357
|
+
// prints this name as it is, never quoted.
|
|
358
|
+
const listed = raw.slice('* Unmerged path '.length)
|
|
359
|
+
current = {
|
|
360
|
+
path: listed !== '' ? listed : null,
|
|
361
|
+
added: false,
|
|
362
|
+
deleted: false,
|
|
363
|
+
binary: false,
|
|
364
|
+
newBlob: null,
|
|
365
|
+
oldPath: null,
|
|
366
|
+
addedLines: [],
|
|
367
|
+
incomplete: 'unmerged-path',
|
|
368
|
+
inHeader: false,
|
|
369
|
+
...(detail ? { hunks: [], lines: [] } : {}),
|
|
370
|
+
}
|
|
371
|
+
sections.push(current)
|
|
201
372
|
continue
|
|
202
373
|
}
|
|
203
|
-
|
|
374
|
+
if (!current) continue
|
|
375
|
+
if (raw.startsWith('\\')) continue
|
|
376
|
+
const hunk = raw.match(HUNK_HEADER)
|
|
204
377
|
if (hunk) {
|
|
205
|
-
|
|
378
|
+
current.inHeader = false
|
|
379
|
+
if (detail) current.hunks.push({ added: [], removed: [] })
|
|
380
|
+
pendingRemoved = hunk[1] === undefined ? 1 : Number(hunk[1])
|
|
381
|
+
lineNumber = Number(hunk[2])
|
|
382
|
+
pendingAdded = hunk[3] === undefined ? 1 : Number(hunk[3])
|
|
383
|
+
continue
|
|
384
|
+
}
|
|
385
|
+
if (raw.startsWith('Binary files ') && raw.endsWith(' differ')) {
|
|
386
|
+
current.inHeader = false
|
|
387
|
+
current.binary = true
|
|
206
388
|
continue
|
|
207
389
|
}
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
390
|
+
if (!current.inHeader) {
|
|
391
|
+
if (raw !== '') current.incomplete ??= 'unexpected-line-outside-a-hunk'
|
|
392
|
+
continue
|
|
393
|
+
}
|
|
394
|
+
const index = raw.match(INDEX_LINE)
|
|
395
|
+
if (index) {
|
|
396
|
+
current.newBlob = ZERO_SHA_RE.test(index[2]) ? null : index[2]
|
|
397
|
+
} else if (raw.startsWith('new file mode ')) current.added = true
|
|
398
|
+
else if (raw.startsWith('deleted file mode ')) current.deleted = true
|
|
399
|
+
else if (raw.startsWith('rename from ') || raw.startsWith('copy from ')) {
|
|
400
|
+
const source = raw.slice(raw.indexOf(' from ') + 6)
|
|
401
|
+
const named = source.startsWith('"') ? readQuotedToken(source, 0) : { value: source, end: source.length }
|
|
402
|
+
if (named && named.end === source.length && named.value !== '') current.oldPath = named.value
|
|
403
|
+
else current.incomplete ??= 'unreadable-rename-source'
|
|
404
|
+
} else if (raw.startsWith('rename to ') || raw.startsWith('copy to ')) {
|
|
405
|
+
const target = raw.slice(raw.indexOf(' to ') + 4)
|
|
406
|
+
const named = target.startsWith('"') ? readQuotedToken(target, 0) : { value: target, end: target.length }
|
|
407
|
+
if (named && named.end === target.length && named.value !== '') current.path = named.value
|
|
408
|
+
else current.incomplete ??= 'unreadable-rename-target'
|
|
409
|
+
} else if (raw.startsWith('+++ ')) {
|
|
410
|
+
const target = raw.slice(4)
|
|
411
|
+
if (target !== '/dev/null' && current.path === null) {
|
|
412
|
+
const named = prefixedPath(target, 'b/')
|
|
413
|
+
if (named !== null && named !== '') current.path = named
|
|
414
|
+
}
|
|
215
415
|
}
|
|
216
416
|
}
|
|
217
|
-
|
|
417
|
+
if (pendingAdded > 0 || pendingRemoved > 0) current.incomplete = 'hunk-shorter-than-its-header'
|
|
418
|
+
for (const section of sections) {
|
|
419
|
+
delete section.inHeader
|
|
420
|
+
if (section.path === null) section.incomplete ??= 'unreadable-file-path'
|
|
421
|
+
}
|
|
422
|
+
return sections
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Parse `git diff` output into added files and added lines with line numbers. */
|
|
426
|
+
export function parseAddedContent(diff) {
|
|
427
|
+
return addedFilesFrom(parseDiffSections(diff))
|
|
218
428
|
}
|
|
219
429
|
|
|
220
430
|
/** Ref updates arrive on the pre-push hook's stdin as `<localRef> <localSha> <remoteRef> <remoteSha>`. */
|
|
@@ -254,12 +464,16 @@ export function parsePushRefInput(text) {
|
|
|
254
464
|
export function gitOutputResult(repoRoot, args, {
|
|
255
465
|
encoding = 'utf8',
|
|
256
466
|
gitExecutable = 'git',
|
|
467
|
+
indexFile = null,
|
|
257
468
|
maxBuffer = DIFF_RESULT_MAX_BYTES,
|
|
258
469
|
runner = spawnSync,
|
|
259
470
|
} = {}) {
|
|
260
471
|
let result
|
|
472
|
+
// A hook names the index being committed (`git commit -a` writes a temporary
|
|
473
|
+
// one); every other variable from the caller's Git environment is dropped.
|
|
474
|
+
const env = indexFile ? { ...sanitizedGitEnvironment(), GIT_INDEX_FILE: indexFile } : sanitizedGitEnvironment()
|
|
261
475
|
try {
|
|
262
|
-
result = runner(gitExecutable, ['-C', repoRoot, ...args], { encoding, maxBuffer, env
|
|
476
|
+
result = runner(gitExecutable, ['-C', repoRoot, ...args], { encoding, maxBuffer, env })
|
|
263
477
|
} catch (error) {
|
|
264
478
|
return { ok: false, code: 'git-command-failed', error: error instanceof Error ? error.message : String(error), stdout: null }
|
|
265
479
|
}
|
|
@@ -277,25 +491,34 @@ export function gitOutputResult(repoRoot, args, {
|
|
|
277
491
|
return { ok: true, stdout, bytes }
|
|
278
492
|
}
|
|
279
493
|
|
|
280
|
-
|
|
494
|
+
/** The empty tree in this repository's object format (SHA-1 or SHA-256). */
|
|
495
|
+
export function emptyTreeFor(repoRoot, { runner = spawnSync } = {}) {
|
|
496
|
+
const result = gitOutputResult(repoRoot, ['rev-parse', '--show-object-format'], { maxBuffer: 1024, runner })
|
|
497
|
+
return (result.ok && EMPTY_TREES[result.stdout.trim()]) || EMPTY_TREE
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export function pushRangeBase(repoRoot, remoteSha, { runner = spawnSync } = {}) {
|
|
281
501
|
// A new branch has no remote counterpart, so everything it carries is new:
|
|
282
502
|
// diff against the empty tree rather than silently scanning nothing.
|
|
283
|
-
if (!remoteSha || ZERO_SHA_RE.test(remoteSha)) return
|
|
284
|
-
const result =
|
|
285
|
-
|
|
286
|
-
maxBuffer: 1024 * 1024,
|
|
287
|
-
})
|
|
288
|
-
return result.status === 0 ? remoteSha : EMPTY_TREE
|
|
503
|
+
if (!remoteSha || ZERO_SHA_RE.test(remoteSha)) return emptyTreeFor(repoRoot, { runner })
|
|
504
|
+
const result = gitOutputResult(repoRoot, ['rev-parse', '--verify', '--quiet', `${remoteSha}^{commit}`], { maxBuffer: 1024 * 1024, runner })
|
|
505
|
+
return result.ok ? remoteSha : emptyTreeFor(repoRoot, { runner })
|
|
289
506
|
}
|
|
290
507
|
|
|
508
|
+
export const canonicalDiffArgs = (revisions, extra = []) => ['diff', ...CANONICAL_DIFF_FLAGS, '--unified=0', ...extra, ...revisions]
|
|
509
|
+
// The content scan judges added lines only, so a deleted file's removed lines
|
|
510
|
+
// would just spend the output limit. The semantic review reads removed lines
|
|
511
|
+
// and keeps the plain form above.
|
|
512
|
+
const contentDiffArgs = (revisions, extra = []) => canonicalDiffArgs(revisions, ['--irreversible-delete', ...extra])
|
|
513
|
+
|
|
291
514
|
export function diffForPushRange(repoRoot, { localSha, remoteSha }, options = {}) {
|
|
292
|
-
const base = pushRangeBase(repoRoot, remoteSha)
|
|
293
|
-
const result = gitOutputResult(repoRoot, [
|
|
515
|
+
const base = pushRangeBase(repoRoot, remoteSha, { runner: options.runner })
|
|
516
|
+
const result = gitOutputResult(repoRoot, contentDiffArgs([base, localSha]), options)
|
|
294
517
|
return result.ok ? { ...result, base, diff: result.stdout } : { ...result, base, diff: null }
|
|
295
518
|
}
|
|
296
519
|
|
|
297
520
|
export function stagedDiff(repoRoot, options = {}) {
|
|
298
|
-
const result = gitOutputResult(repoRoot, ['
|
|
521
|
+
const result = gitOutputResult(repoRoot, contentDiffArgs(['--cached']), options)
|
|
299
522
|
return result.ok ? { ...result, diff: result.stdout } : { ...result, diff: null }
|
|
300
523
|
}
|
|
301
524
|
|
|
@@ -311,20 +534,21 @@ function incompleteDiagnostic({ repo, path: filePath = null, message, details =
|
|
|
311
534
|
}
|
|
312
535
|
}
|
|
313
536
|
|
|
314
|
-
function
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
if (current && (line.startsWith('Binary files ') || line === 'GIT binary patch')) paths.add(current)
|
|
324
|
-
}
|
|
325
|
-
return [...paths]
|
|
537
|
+
function unreadableSectionDiagnostics(sections, repo) {
|
|
538
|
+
return sections
|
|
539
|
+
.filter((section) => section.incomplete)
|
|
540
|
+
.map((section) => incompleteDiagnostic({
|
|
541
|
+
repo,
|
|
542
|
+
path: section.path,
|
|
543
|
+
message: 'a changed file could not be read from the diff, so its content was not scanned',
|
|
544
|
+
details: { reason: section.incomplete },
|
|
545
|
+
}))
|
|
326
546
|
}
|
|
327
547
|
|
|
548
|
+
const addedFilesFrom = (sections) => sections
|
|
549
|
+
.filter((section) => section.path !== null)
|
|
550
|
+
.map(({ path: filePath, added, addedLines }) => ({ path: filePath, added, addedLines }))
|
|
551
|
+
|
|
328
552
|
function isBinaryBuffer(buffer) {
|
|
329
553
|
if (buffer.subarray(0, Math.min(buffer.length, 8192)).includes(0)) return true
|
|
330
554
|
try {
|
|
@@ -336,7 +560,6 @@ function isBinaryBuffer(buffer) {
|
|
|
336
560
|
}
|
|
337
561
|
|
|
338
562
|
function scanBinaryBuffer({ buffer, filePath, rules, exceptions, repo }) {
|
|
339
|
-
if (!isBinaryBuffer(buffer)) return []
|
|
340
563
|
const text = buffer.toString('latin1')
|
|
341
564
|
const findings = []
|
|
342
565
|
for (const rule of rules) {
|
|
@@ -357,13 +580,100 @@ function scanBinaryBuffer({ buffer, filePath, rules, exceptions, repo }) {
|
|
|
357
580
|
return findings
|
|
358
581
|
}
|
|
359
582
|
|
|
360
|
-
|
|
583
|
+
const DECLARED_TEXT_BATCH = 200
|
|
584
|
+
// A new file's diff can be twice its size (a "+" before every short line), so a
|
|
585
|
+
// batch holds at most half the diff limit in blob bytes.
|
|
586
|
+
const DECLARED_TEXT_BATCH_BYTES = DIFF_RESULT_MAX_BYTES / 2
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Git reports a file as binary when its attributes say so, whatever it holds
|
|
590
|
+
* (for example `*.cfg binary` in .gitattributes, which can arrive with the tree),
|
|
591
|
+
* or when the content it replaces was binary.
|
|
592
|
+
* Such text is judged like any other text: by its added lines, taken from a
|
|
593
|
+
* `--text` diff of just those paths and their rename sources. Narrowing the
|
|
594
|
+
* paths can change how Git pairs renames and copies, so a file's lines are used
|
|
595
|
+
* only when it is paired exactly as in the main diff; otherwise, or when a batch
|
|
596
|
+
* cannot be read, the file is read again on its own. Pathspecs are taken
|
|
597
|
+
* literally: core.precomposeUnicode is off for this call, because it would
|
|
598
|
+
* rewrite a decomposed name on the command line and match nothing. Returns, per
|
|
599
|
+
* path, `{ addedLines }` or `{ cause }` naming why the text was not read.
|
|
600
|
+
*/
|
|
601
|
+
function declaredBinaryTextLines({ repoRoot, revisions, entries, changedPaths = [], gitExecutable, gitRunner, indexFile = null }) {
|
|
602
|
+
const outcomes = new Map()
|
|
603
|
+
const read = (group) => {
|
|
604
|
+
const paths = [...new Set(group.flatMap(({ section }) => [section.path, section.oldPath].filter((value) => value && value !== '')))]
|
|
605
|
+
// A literal pathspec also matches a directory of that name, so a file that
|
|
606
|
+
// replaced a directory would bring the directory's changed files in as
|
|
607
|
+
// rename candidates and as output. Only changed entries can appear in a
|
|
608
|
+
// diff. Under a read path with nothing else read inside, one subtree
|
|
609
|
+
// exclusion covers them all; under one that holds another read path, the
|
|
610
|
+
// changed entries not read are excluded one by one. Usually there are none.
|
|
611
|
+
const reading = new Set(paths)
|
|
612
|
+
const excluded = new Set()
|
|
613
|
+
for (const read of paths) {
|
|
614
|
+
const under = changedPaths.filter((value) => value.startsWith(`${read}/`))
|
|
615
|
+
if (under.length === 0) continue
|
|
616
|
+
if (!paths.some((other) => other.startsWith(`${read}/`))) excluded.add(`${read}/`)
|
|
617
|
+
else for (const value of under) if (!reading.has(value)) excluded.add(value)
|
|
618
|
+
}
|
|
619
|
+
const result = gitOutputResult(repoRoot, [
|
|
620
|
+
'-c', 'core.precomposeUnicode=false',
|
|
621
|
+
...contentDiffArgs(revisions, ['--text']),
|
|
622
|
+
'--',
|
|
623
|
+
...paths.map((value) => `:(literal)${value}`),
|
|
624
|
+
...[...excluded].map((value) => `:(exclude,literal)${value}`),
|
|
625
|
+
], { gitExecutable, indexFile, runner: gitRunner })
|
|
626
|
+
const parsed = result.ok ? parseDiffSections(result.stdout) : []
|
|
627
|
+
for (const { section } of group) {
|
|
628
|
+
const matches = parsed.filter((candidate) => candidate.path === section.path && !candidate.deleted)
|
|
629
|
+
let cause = null
|
|
630
|
+
if (!result.ok) cause = result.code
|
|
631
|
+
else if (matches.length === 0) cause = 'missing-from-text-diff'
|
|
632
|
+
else if (matches.some((candidate) => candidate.oldPath !== section.oldPath)) cause = 'paired-differently-from-main-diff'
|
|
633
|
+
else cause = matches.find((candidate) => candidate.incomplete)?.incomplete ?? (matches.some((candidate) => candidate.binary) ? 'binary-in-text-diff' : null)
|
|
634
|
+
outcomes.set(section.path, cause ? { cause } : { addedLines: matches.flatMap((candidate) => candidate.addedLines) })
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const groups = []
|
|
638
|
+
let group = []
|
|
639
|
+
let bytes = 0
|
|
640
|
+
for (const entry of entries) {
|
|
641
|
+
if (group.length > 0 && (group.length >= DECLARED_TEXT_BATCH || bytes + entry.bytes > DECLARED_TEXT_BATCH_BYTES)) {
|
|
642
|
+
groups.push(group)
|
|
643
|
+
group = []
|
|
644
|
+
bytes = 0
|
|
645
|
+
}
|
|
646
|
+
group.push(entry)
|
|
647
|
+
bytes += entry.bytes
|
|
648
|
+
}
|
|
649
|
+
if (group.length > 0) groups.push(group)
|
|
650
|
+
for (const batch of groups) {
|
|
651
|
+
read(batch)
|
|
652
|
+
if (batch.length === 1) continue
|
|
653
|
+
for (const entry of batch) if (outcomes.get(entry.section.path).cause) read([entry])
|
|
654
|
+
}
|
|
655
|
+
return outcomes
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function readBinaryChanges({ repoRoot, revisions, sections, rules, exceptions, repo, gitExecutable, gitRunner, indexFile = null }) {
|
|
361
659
|
const findings = []
|
|
362
660
|
const diagnostics = []
|
|
363
661
|
let totalBytes = 0
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
662
|
+
const blobs = []
|
|
663
|
+
const seen = new Set()
|
|
664
|
+
const declaredText = []
|
|
665
|
+
// A deleted path is absent from the revision being scanned, so there is nothing
|
|
666
|
+
// to read: the blob it had is already in history and is not being introduced.
|
|
667
|
+
for (const section of sections) {
|
|
668
|
+
if (!section.binary || section.deleted || section.path === null || seen.has(section.path)) continue
|
|
669
|
+
seen.add(section.path)
|
|
670
|
+
const filePath = section.path
|
|
671
|
+
if (!section.newBlob) {
|
|
672
|
+
diagnostics.push(incompleteDiagnostic({ repo, path: filePath, message: 'binary content could not be identified in the diff', details: { reason: 'missing-blob-id' } }))
|
|
673
|
+
continue
|
|
674
|
+
}
|
|
675
|
+
// Read by object id: a path can be misread (":0:name" names an index stage).
|
|
676
|
+
const result = gitOutputResult(repoRoot, ['cat-file', 'blob', section.newBlob], {
|
|
367
677
|
encoding: 'buffer',
|
|
368
678
|
gitExecutable,
|
|
369
679
|
maxBuffer: BINARY_FILE_MAX_BYTES + 1,
|
|
@@ -388,6 +698,7 @@ function readBinaryChanges({ repoRoot, revision, diff, rules, exceptions, repo,
|
|
|
388
698
|
continue
|
|
389
699
|
}
|
|
390
700
|
totalBytes += result.bytes
|
|
701
|
+
blobs.push({ id: section.newBlob, bytes: result.bytes })
|
|
391
702
|
if (totalBytes > BINARY_AGGREGATE_MAX_BYTES) {
|
|
392
703
|
diagnostics.push(incompleteDiagnostic({
|
|
393
704
|
repo,
|
|
@@ -397,9 +708,33 @@ function readBinaryChanges({ repoRoot, revision, diff, rules, exceptions, repo,
|
|
|
397
708
|
}))
|
|
398
709
|
break
|
|
399
710
|
}
|
|
400
|
-
|
|
711
|
+
if (isBinaryBuffer(result.stdout)) {
|
|
712
|
+
findings.push(...scanBinaryBuffer({ buffer: result.stdout, filePath, rules, exceptions, repo }))
|
|
713
|
+
continue
|
|
714
|
+
}
|
|
715
|
+
declaredText.push({ section, bytes: result.bytes })
|
|
716
|
+
}
|
|
717
|
+
if (declaredText.length > 0) {
|
|
718
|
+
const changedPaths = [...new Set(sections.flatMap((section) => [section.path, section.oldPath]).filter((value) => value && value !== ''))]
|
|
719
|
+
const outcomes = declaredBinaryTextLines({ repoRoot, revisions, entries: declaredText, changedPaths, gitExecutable, gitRunner, indexFile })
|
|
720
|
+
const contentRules = rules.filter((rule) => (rule.kind ?? 'content') === 'content')
|
|
721
|
+
for (const { section } of declaredText) {
|
|
722
|
+
const { addedLines, cause } = outcomes.get(section.path)
|
|
723
|
+
if (cause) {
|
|
724
|
+
diagnostics.push(incompleteDiagnostic({
|
|
725
|
+
repo,
|
|
726
|
+
path: section.path,
|
|
727
|
+
message: cause === 'git-output-limit-exceeded'
|
|
728
|
+
? `text that Git reports as binary has a diff over the ${DIFF_RESULT_MAX_BYTES}-byte limit, so its content was not scanned`
|
|
729
|
+
: 'text that Git reports as binary could not be read as text, so its content was not scanned',
|
|
730
|
+
details: { reason: 'declared-binary-text-unreadable', cause },
|
|
731
|
+
}))
|
|
732
|
+
continue
|
|
733
|
+
}
|
|
734
|
+
findings.push(...scanAddedContent({ files: [{ path: section.path, added: false, addedLines }], rules: contentRules, exceptions, repo }))
|
|
735
|
+
}
|
|
401
736
|
}
|
|
402
|
-
return { findings, diagnostics, bytes: totalBytes }
|
|
737
|
+
return { findings, diagnostics, bytes: totalBytes, blobs }
|
|
403
738
|
}
|
|
404
739
|
|
|
405
740
|
export function scanStagedRepository({
|
|
@@ -409,8 +744,9 @@ export function scanStagedRepository({
|
|
|
409
744
|
repo = null,
|
|
410
745
|
gitExecutable = 'git',
|
|
411
746
|
gitRunner = spawnSync,
|
|
747
|
+
indexFile = null,
|
|
412
748
|
} = {}) {
|
|
413
|
-
const acquired = stagedDiff(repoRoot, { gitExecutable, runner: gitRunner })
|
|
749
|
+
const acquired = stagedDiff(repoRoot, { gitExecutable, indexFile, runner: gitRunner })
|
|
414
750
|
if (!acquired.ok) {
|
|
415
751
|
return {
|
|
416
752
|
findings: [],
|
|
@@ -418,38 +754,169 @@ export function scanStagedRepository({
|
|
|
418
754
|
bytes: 0,
|
|
419
755
|
}
|
|
420
756
|
}
|
|
421
|
-
const
|
|
422
|
-
const binary = readBinaryChanges({ repoRoot,
|
|
757
|
+
const sections = parseDiffSections(acquired.diff)
|
|
758
|
+
const binary = readBinaryChanges({ repoRoot, revisions: ['--cached'], sections, rules, exceptions, repo, gitExecutable, gitRunner, indexFile })
|
|
423
759
|
return {
|
|
424
|
-
findings: [...scanAddedContent({ files, rules, exceptions, repo }), ...binary.findings],
|
|
425
|
-
diagnostics: binary.diagnostics,
|
|
760
|
+
findings: [...scanAddedContent({ files: addedFilesFrom(sections), rules, exceptions, repo }), ...binary.findings],
|
|
761
|
+
diagnostics: [...unreadableSectionDiagnostics(sections, repo), ...binary.diagnostics],
|
|
426
762
|
bytes: acquired.bytes + binary.bytes,
|
|
427
763
|
}
|
|
428
764
|
}
|
|
429
765
|
|
|
766
|
+
function scanRevisionRange({ repoRoot, base, tip, rules, exceptions, repo, gitRunner }) {
|
|
767
|
+
const acquired = gitOutputResult(repoRoot, contentDiffArgs([base, tip]), { runner: gitRunner })
|
|
768
|
+
if (!acquired.ok) return { acquired, keyed: [], diagnostics: [], bytes: 0, blobs: [] }
|
|
769
|
+
const sections = parseDiffSections(acquired.stdout)
|
|
770
|
+
const binary = readBinaryChanges({ repoRoot, revisions: [base, tip], sections, rules, exceptions, repo, gitRunner })
|
|
771
|
+
const findings = [...scanAddedContent({ files: addedFilesFrom(sections), rules, exceptions, repo }), ...binary.findings]
|
|
772
|
+
return {
|
|
773
|
+
acquired,
|
|
774
|
+
keyed: findings.map((item) => [findingKey(item), item]),
|
|
775
|
+
diagnostics: [...unreadableSectionDiagnostics(sections, repo), ...binary.diagnostics],
|
|
776
|
+
bytes: acquired.bytes + binary.bytes,
|
|
777
|
+
blobs: binary.blobs,
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// What a finding reports: its rule, path and the digest of its line (binary
|
|
782
|
+
// content has no line), so the same content is recognised in any diff.
|
|
783
|
+
function findingKey(finding) {
|
|
784
|
+
return [finding.rule, finding.path, finding.lineDigest ?? 'binary'].join('\0')
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* What one commit adds. A merge adds only what is new against every parent:
|
|
789
|
+
* a side parent's own commits are judged where they were made, and content the
|
|
790
|
+
* remote already holds is not judged again. A finding is dropped only when a
|
|
791
|
+
* fully read diff against another parent shows that parent already had it, and
|
|
792
|
+
* one fully read parent diff covers everything new against all parents.
|
|
793
|
+
*/
|
|
794
|
+
function scanCommit({ repoRoot, commit, parents, empty, rules, exceptions, repo, gitRunner }) {
|
|
795
|
+
if (parents.length <= 1) return scanRevisionRange({ repoRoot, base: parents[0] ?? empty, tip: commit, rules, exceptions, repo, gitRunner })
|
|
796
|
+
const read = parents
|
|
797
|
+
.map((parent) => scanRevisionRange({ repoRoot, base: parent, tip: commit, rules, exceptions, repo, gitRunner }))
|
|
798
|
+
.filter((scan) => scan.acquired.ok)
|
|
799
|
+
if (read.length === 0) return { acquired: { ok: false, code: 'merge-unreadable' }, keyed: [], diagnostics: [], bytes: 0, blobs: [] }
|
|
800
|
+
const complete = read.filter((scan) => scan.diagnostics.length === 0)
|
|
801
|
+
// Any complete parent diff yields the same findings; the smallest is the one
|
|
802
|
+
// that does not re-read what the other parent already published.
|
|
803
|
+
const reference = complete.length > 0 ? complete.reduce((small, scan) => (scan.bytes < small.bytes ? scan : small)) : read[0]
|
|
804
|
+
const keyed = reference.keyed.filter(([key]) => complete.every((scan) => scan === reference || scan.keyed.some(([other]) => other === key)))
|
|
805
|
+
const diagnostics = complete.length > 0 ? [] : read.flatMap((scan) => scan.diagnostics)
|
|
806
|
+
// Evidence counts once, as the diff the commit is judged by.
|
|
807
|
+
return { acquired: reference.acquired, keyed, diagnostics, bytes: reference.bytes, blobs: reference.blobs }
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const PLAIN_REMOTE_NAME = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Which tracking refs already hold what this push carries. The installed hook
|
|
814
|
+
* passes the remote Git names, which is a configured remote or a URL; a URL is
|
|
815
|
+
* matched to the remote configured with it. Only that remote's tracking refs
|
|
816
|
+
* exclude commits, so history held only by another remote is judged on its way
|
|
817
|
+
* here. A URL no remote is configured with excludes nothing but the remote's
|
|
818
|
+
* own commit. With no remote named (a hook installed before this), every
|
|
819
|
+
* remote's tracking refs exclude, as before.
|
|
820
|
+
*/
|
|
821
|
+
function remoteScope(repoRoot, remote, runner) {
|
|
822
|
+
if (typeof remote !== 'string' || remote === '') return { args: ['--remotes'], label: 'no remote holds' }
|
|
823
|
+
const configured = gitOutputResult(repoRoot, ['config', '--get-regexp', '^remote\\..*\\.url$'], { maxBuffer: 1024 * 1024, runner })
|
|
824
|
+
const remotes = configured.ok
|
|
825
|
+
? configured.stdout.split('\n').filter(Boolean).map((line) => {
|
|
826
|
+
const space = line.indexOf(' ')
|
|
827
|
+
return { name: line.slice('remote.'.length, space).replace(/\.url$/, ''), url: line.slice(space + 1) }
|
|
828
|
+
})
|
|
829
|
+
: []
|
|
830
|
+
const named = remotes.find((item) => item.name === remote) ?? remotes.find((item) => item.url === remote)
|
|
831
|
+
if (named && PLAIN_REMOTE_NAME.test(named.name)) {
|
|
832
|
+
// `--remotes=origin` would also take a remote named `origin/x`; leave those out.
|
|
833
|
+
const nested = remotes.filter((item) => item.name.startsWith(`${named.name}/`)).map((item) => `--exclude=${item.name}/*`)
|
|
834
|
+
return { args: [...nested, `--remotes=${named.name}`], label: `${named.name} does not hold` }
|
|
835
|
+
}
|
|
836
|
+
return { args: [], label: `${remote} does not hold` }
|
|
837
|
+
}
|
|
838
|
+
|
|
430
839
|
export function scanPushUpdate({
|
|
431
840
|
repoRoot,
|
|
432
841
|
update,
|
|
433
842
|
rules = DEFAULT_CONTENT_RULES,
|
|
434
843
|
exceptions = [],
|
|
435
844
|
repo = null,
|
|
845
|
+
remote = null,
|
|
436
846
|
gitRunner = spawnSync,
|
|
847
|
+
maxCommits = PUSH_COMMIT_SCAN_MAX,
|
|
848
|
+
maxBytes = CHECK_AGGREGATE_MAX_BYTES,
|
|
437
849
|
} = {}) {
|
|
438
|
-
const
|
|
439
|
-
|
|
850
|
+
const base = pushRangeBase(repoRoot, update.remoteSha, { runner: gitRunner })
|
|
851
|
+
const net = scanRevisionRange({ repoRoot, base, tip: update.localSha, rules, exceptions, repo, gitRunner })
|
|
852
|
+
if (!net.acquired.ok) {
|
|
440
853
|
return {
|
|
441
854
|
findings: [],
|
|
442
|
-
diagnostics: [incompleteDiagnostic({ repo, message: 'push evidence could not be acquired', details: { reason: acquired.code } })],
|
|
855
|
+
diagnostics: [incompleteDiagnostic({ repo, message: 'push evidence could not be acquired', details: { reason: net.acquired.code } })],
|
|
443
856
|
bytes: 0,
|
|
444
857
|
}
|
|
445
858
|
}
|
|
446
|
-
const
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
859
|
+
const reported = new Map(net.keyed)
|
|
860
|
+
const findings = net.keyed.map(([, item]) => item)
|
|
861
|
+
const diagnostics = [...net.diagnostics]
|
|
862
|
+
// Every path is judged wherever it appears, but each blob counts once toward
|
|
863
|
+
// the evidence budget, however many diffs read it.
|
|
864
|
+
const counted = new Set()
|
|
865
|
+
const evidence = (scan) => scan.bytes - scan.blobs.reduce((repeat, blob) => {
|
|
866
|
+
if (counted.has(blob.id)) return repeat + blob.bytes
|
|
867
|
+
counted.add(blob.id)
|
|
868
|
+
return repeat
|
|
869
|
+
}, 0)
|
|
870
|
+
let bytes = evidence(net)
|
|
871
|
+
const scope = remoteScope(repoRoot, remote, gitRunner)
|
|
872
|
+
// Parents before children, so a commit is judged before a merge that brings it in.
|
|
873
|
+
const listed = gitOutputResult(repoRoot, ['rev-list', '--topo-order', '--reverse', '--parents', `--max-count=${maxCommits + 1}`, update.localSha, '--not', ...(base === update.remoteSha ? [base] : []), ...scope.args], { runner: gitRunner })
|
|
874
|
+
if (!listed.ok) {
|
|
875
|
+
diagnostics.push(incompleteDiagnostic({ repo, message: 'the commits in this push could not be listed', details: { reason: listed.code } }))
|
|
876
|
+
return { findings, diagnostics, bytes }
|
|
877
|
+
}
|
|
878
|
+
const commits = listed.stdout.split('\n').filter(Boolean).map((line) => line.split(' '))
|
|
879
|
+
if (commits.length > maxCommits) {
|
|
880
|
+
diagnostics.push(incompleteDiagnostic({
|
|
881
|
+
repo,
|
|
882
|
+
message: `this push carries more than ${maxCommits} commits that ${scope.label}, more than are judged one by one; push it in smaller parts, an earlier commit first (git push <remote> <commit>:<ref>)`,
|
|
883
|
+
details: { reason: 'push-range-too-long', limit: maxCommits },
|
|
884
|
+
}))
|
|
885
|
+
return { findings, diagnostics, bytes }
|
|
886
|
+
}
|
|
887
|
+
// One commit straight on the remote's tip is the endpoint change already judged.
|
|
888
|
+
if (commits.length === 1 && commits[0].length === 2 && commits[0][1] === base) return { findings, diagnostics, bytes }
|
|
889
|
+
const empty = emptyTreeFor(repoRoot, { runner: gitRunner })
|
|
890
|
+
for (const [commit, ...parents] of commits) {
|
|
891
|
+
const step = scanCommit({ repoRoot, commit, parents, empty, rules, exceptions, repo, gitRunner })
|
|
892
|
+
const short = commit.slice(0, 12)
|
|
893
|
+
if (!step.acquired.ok) {
|
|
894
|
+
diagnostics.push(incompleteDiagnostic({ repo, message: `commit ${short} could not be read`, details: { reason: step.acquired.code, commit } }))
|
|
895
|
+
continue
|
|
896
|
+
}
|
|
897
|
+
bytes += evidence(step)
|
|
898
|
+
for (const [key, item] of step.keyed) {
|
|
899
|
+
const retained = reported.get(key)
|
|
900
|
+
if (retained) {
|
|
901
|
+
// Already reported: record where else it was published.
|
|
902
|
+
if (!retained.commit) retained.commits = [...(retained.commits ?? []), commit]
|
|
903
|
+
continue
|
|
904
|
+
}
|
|
905
|
+
const placed = { ...item, commit, message: `${item.message} (in commit ${short})` }
|
|
906
|
+
reported.set(key, placed)
|
|
907
|
+
findings.push(placed)
|
|
908
|
+
}
|
|
909
|
+
diagnostics.push(...step.diagnostics.map((item) => ({ ...item, message: `${item.message} (in commit ${short})`, details: { ...item.details, commit } })))
|
|
910
|
+
if (bytes > maxBytes) {
|
|
911
|
+
diagnostics.push(incompleteDiagnostic({
|
|
912
|
+
repo,
|
|
913
|
+
message: `push evidence exceeds the ${maxBytes}-byte aggregate limit; push it in smaller parts`,
|
|
914
|
+
details: { reason: 'push-evidence-too-large', limitBytes: maxBytes, commit },
|
|
915
|
+
}))
|
|
916
|
+
break
|
|
917
|
+
}
|
|
452
918
|
}
|
|
919
|
+
return { findings, diagnostics, bytes }
|
|
453
920
|
}
|
|
454
921
|
|
|
455
922
|
/**
|