@jaimevalasek/aioson 1.7.2 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.md +153 -10
- package/docs/en/cli-reference.md +56 -1
- package/docs/en/i18n.md +18 -18
- package/docs/en/schemas/index.json +10 -0
- package/docs/en/schemas/parallel-assign.schema.json +9 -0
- package/docs/en/schemas/parallel-doctor.schema.json +36 -0
- package/docs/en/schemas/parallel-guard.schema.json +63 -0
- package/docs/en/schemas/parallel-merge.schema.json +84 -0
- package/docs/en/schemas/parallel-status.schema.json +91 -1
- package/docs/integrations/apps-publish-marketplace.md +94 -0
- package/docs/pt/README.md +9 -0
- package/docs/pt/agentes.md +324 -3
- package/docs/pt/clientes-ai.md +7 -3
- package/docs/pt/comandos-cli.md +160 -13
- package/docs/pt/compress-agents.md +304 -0
- package/docs/pt/design-docs-governance.md +59 -0
- package/docs/pt/feature-archive.md +191 -0
- package/docs/pt/genome-3.0-spec.md +115 -4
- package/docs/pt/genome-distribution.md +232 -0
- package/docs/pt/inicio-rapido.md +1 -0
- package/docs/pt/motor-hardening.md +492 -0
- package/docs/pt/runner-system.md +113 -0
- package/package.json +2 -1
- package/src/agent-manifests.js +66 -0
- package/src/agents.js +27 -7
- package/src/autonomy-policy.js +139 -0
- package/src/brain-query.js +161 -0
- package/src/cli.js +1377 -1099
- package/src/commands/agents.js +102 -7
- package/src/commands/artifact-validate.js +33 -4
- package/src/commands/auth.js +272 -0
- package/src/commands/brain-query.js +44 -0
- package/src/commands/briefing.js +344 -0
- package/src/commands/commit-prepare.js +547 -0
- package/src/commands/compress-agents.js +416 -0
- package/src/commands/context-health.js +4 -2
- package/src/commands/context-trim.js +17 -11
- package/src/commands/design-hybrid-options.js +3 -3
- package/src/commands/devlog-process.js +6 -4
- package/src/commands/dossier.js +423 -0
- package/src/commands/feature-archive.js +513 -0
- package/src/commands/feature-close.js +123 -18
- package/src/commands/gate-approve.js +198 -0
- package/src/commands/gate-check.js +24 -5
- package/src/commands/genome-doctor.js +166 -9
- package/src/commands/git-guard.js +170 -0
- package/src/commands/harness.js +121 -0
- package/src/commands/implementation-plan.js +47 -20
- package/src/commands/init.js +6 -2
- package/src/commands/install.js +6 -2
- package/src/commands/live.js +497 -56
- package/src/commands/locale-apply.js +9 -6
- package/src/commands/locale-diff.js +11 -112
- package/src/commands/mcp-doctor.js +2 -1
- package/src/commands/mcp-init.js +4 -10
- package/src/commands/memory.js +234 -0
- package/src/commands/parallel-assign.js +107 -27
- package/src/commands/parallel-doctor.js +416 -3
- package/src/commands/parallel-guard.js +241 -0
- package/src/commands/parallel-init.js +66 -4
- package/src/commands/parallel-merge.js +299 -0
- package/src/commands/parallel-status.js +147 -3
- package/src/commands/preflight.js +63 -4
- package/src/commands/qa-init.js +10 -5
- package/src/commands/revision.js +235 -0
- package/src/commands/scaffold-complete.js +188 -0
- package/src/commands/security-audit.js +275 -0
- package/src/commands/security-scan.js +376 -0
- package/src/commands/self-implement-loop.js +46 -2
- package/src/commands/setup-context.js +11 -10
- package/src/commands/squad-agent-create.js +51 -9
- package/src/commands/squad-investigate.js +53 -0
- package/src/commands/squad-plan.js +33 -1
- package/src/commands/squad-scaffold.js +4 -3
- package/src/commands/squad-score.js +71 -14
- package/src/commands/squad-status.js +22 -1
- package/src/commands/squad-validate.js +93 -2
- package/src/commands/store-genome.js +304 -0
- package/src/commands/store-skill.js +247 -0
- package/src/commands/store-squad.js +431 -0
- package/src/commands/store-system.js +392 -0
- package/src/commands/tool-capabilities.js +63 -0
- package/src/commands/update.js +3 -3
- package/src/commands/verify-gate.js +40 -0
- package/src/commands/workflow-execute.js +644 -155
- package/src/commands/workflow-harden.js +231 -0
- package/src/commands/workflow-heal.js +136 -0
- package/src/commands/workflow-next.js +460 -22
- package/src/commands/workflow-status.js +328 -138
- package/src/commands/workspace.js +144 -0
- package/src/constants.js +42 -75
- package/src/context-memory.js +133 -4
- package/src/context-writer.js +2 -1
- package/src/context.js +32 -2
- package/src/doctor.js +46 -6
- package/src/dossier/codemap-store.js +267 -0
- package/src/dossier/dossier-bootstrap.js +222 -0
- package/src/dossier/dossier-compact.js +159 -0
- package/src/dossier/lock.js +128 -0
- package/src/dossier/revision-store.js +313 -0
- package/src/dossier/schema.js +155 -0
- package/src/dossier/store.js +400 -0
- package/src/execution-gateway.js +3 -0
- package/src/friction-scanner.js +202 -0
- package/src/genome-schema.js +24 -1
- package/src/genomes.js +33 -0
- package/src/handoff-contract.js +363 -0
- package/src/handoff-validator.js +45 -0
- package/src/harness/circuit-breaker.js +135 -0
- package/src/i18n/messages/en.js +317 -22
- package/src/i18n/messages/es.js +259 -18
- package/src/i18n/messages/fr.js +260 -18
- package/src/i18n/messages/pt-BR.js +313 -22
- package/src/install-profile.js +0 -16
- package/src/installer.js +70 -6
- package/src/lib/git-commit-guard.js +691 -0
- package/src/lib/security/artifact-reader.js +167 -0
- package/src/lib/security/exit-codes.js +51 -0
- package/src/lib/security/findings-writer.js +176 -0
- package/src/lib/security/runtime-events.js +77 -0
- package/src/lib/security/secrets-regex.js +115 -0
- package/src/lib/store/security-scan.js +173 -0
- package/src/lib/terminal-checkbox.js +130 -0
- package/src/lib/tmux-launcher.js +163 -0
- package/src/lib/tool-capabilities.js +102 -0
- package/src/locales.js +12 -8
- package/src/parallel-workspace.js +756 -0
- package/src/parser.js +8 -1
- package/src/path-guard.js +47 -0
- package/src/preflight-engine.js +237 -26
- package/src/self-healing.js +142 -0
- package/src/session-handoff.js +111 -1
- package/src/squad/squad-scaffold.js +183 -19
- package/src/test-briefing.js +226 -0
- package/src/updater.js +1 -1
- package/src/utils.js +3 -0
- package/src/workflow-gates.js +185 -0
- package/template/.aioson/agents/analyst.md +76 -130
- package/template/.aioson/agents/architect.md +53 -86
- package/template/.aioson/agents/committer.md +161 -0
- package/template/.aioson/agents/cypher.md +252 -0
- package/template/.aioson/agents/dev.md +112 -628
- package/template/.aioson/agents/deyvin.md +33 -236
- package/template/.aioson/agents/discover.md +235 -0
- package/template/.aioson/agents/discovery-design-doc.md +17 -252
- package/template/.aioson/agents/genome.md +76 -26
- package/template/.aioson/agents/manifests/analyst.manifest.json +26 -0
- package/template/.aioson/agents/manifests/architect.manifest.json +23 -0
- package/template/.aioson/agents/manifests/committer.manifest.json +23 -0
- package/template/.aioson/agents/manifests/dev.manifest.json +37 -0
- package/template/.aioson/agents/manifests/orchestrator.manifest.json +30 -0
- package/template/.aioson/agents/manifests/pentester.manifest.json +39 -0
- package/template/.aioson/agents/manifests/pm.manifest.json +26 -0
- package/template/.aioson/agents/manifests/product.manifest.json +23 -0
- package/template/.aioson/agents/manifests/qa.manifest.json +25 -0
- package/template/.aioson/agents/manifests/setup.manifest.json +20 -0
- package/template/.aioson/agents/manifests/ux-ui.manifest.json +24 -0
- package/template/.aioson/agents/neo.md +5 -7
- package/template/.aioson/agents/orache.md +2 -6
- package/template/.aioson/agents/orchestrator.md +81 -182
- package/template/.aioson/agents/pentester.md +235 -0
- package/template/.aioson/agents/pm.md +40 -104
- package/template/.aioson/agents/product.md +99 -344
- package/template/.aioson/agents/profiler-enricher.md +57 -6
- package/template/.aioson/agents/profiler-forge.md +17 -7
- package/template/.aioson/agents/profiler-researcher.md +29 -6
- package/template/.aioson/agents/qa.md +168 -514
- package/template/.aioson/agents/setup.md +52 -278
- package/template/.aioson/agents/sheldon.md +122 -754
- package/template/.aioson/agents/site-forge.md +111 -1583
- package/template/.aioson/agents/squad.md +139 -2010
- package/template/.aioson/agents/tester.md +10 -0
- package/template/.aioson/agents/ux-ui.md +104 -812
- package/template/.aioson/agents/validator.md +69 -0
- package/template/.aioson/brains/scripts/query.js +5 -1
- package/template/.aioson/config/autonomy-protocol.json +43 -0
- package/template/.aioson/config.md +43 -15
- package/template/.aioson/constitution.md +36 -33
- package/template/.aioson/context/design-doc.md +136 -0
- package/template/.aioson/context/project-map.md +57 -0
- package/template/.aioson/design-docs/code-reuse.md +48 -0
- package/template/.aioson/design-docs/componentization.md +47 -0
- package/template/.aioson/design-docs/file-size.md +52 -0
- package/template/.aioson/design-docs/folder-structure.md +51 -0
- package/template/.aioson/design-docs/naming.md +54 -0
- package/template/.aioson/docs/LAYERS.md +12 -2
- package/template/.aioson/docs/dev/execution-discipline.md +106 -0
- package/template/.aioson/docs/dev/stack-conventions.md +83 -0
- package/template/.aioson/docs/deyvin/continuity-recovery.md +57 -0
- package/template/.aioson/docs/deyvin/debugging-escalation.md +30 -0
- package/template/.aioson/docs/deyvin/pair-execution.md +44 -0
- package/template/.aioson/docs/deyvin/runtime-handoffs.md +36 -0
- package/template/.aioson/docs/product/conversation-playbook.md +116 -0
- package/template/.aioson/docs/product/prd-contract.md +107 -0
- package/template/.aioson/docs/product/quality-lens.md +57 -0
- package/template/.aioson/docs/product/research-loop.md +65 -0
- package/template/.aioson/docs/sheldon/enrichment-paths.md +134 -0
- package/template/.aioson/docs/sheldon/quality-lens.md +57 -0
- package/template/.aioson/docs/sheldon/research-loop.md +56 -0
- package/template/.aioson/docs/sheldon/web-intelligence.md +75 -0
- package/template/.aioson/docs/site-forge-build.md +195 -0
- package/template/.aioson/docs/site-forge-extraction.md +135 -0
- package/template/.aioson/docs/site-forge-qa.md +155 -0
- package/template/.aioson/docs/site-forge-recon.md +434 -0
- package/template/.aioson/docs/site-forge-transform.md +249 -0
- package/template/.aioson/docs/squad/content-output.md +91 -0
- package/template/.aioson/docs/squad/creation-flow.md +135 -0
- package/template/.aioson/docs/squad/domain-classification.md +117 -0
- package/template/.aioson/docs/squad/genome-bindings.md +47 -0
- package/template/.aioson/docs/squad/package-contract.md +234 -0
- package/template/.aioson/docs/squad/quality-lens.md +56 -0
- package/template/.aioson/docs/squad/research-loop.md +59 -0
- package/template/.aioson/docs/squad/session-operations.md +117 -0
- package/template/.aioson/docs/squad/workflow-quality.md +165 -0
- package/template/.aioson/docs/ux-ui/accessibility-audit.md +55 -0
- package/template/.aioson/docs/ux-ui/audit-mode.md +86 -0
- package/template/.aioson/docs/ux-ui/component-map.md +35 -0
- package/template/.aioson/docs/ux-ui/design-execution.md +111 -0
- package/template/.aioson/docs/ux-ui/design-gate.md +27 -0
- package/template/.aioson/docs/ux-ui/research-mode.md +39 -0
- package/template/.aioson/docs/ux-ui/site-delivery.md +156 -0
- package/template/.aioson/docs/ux-ui/token-contract.md +57 -0
- package/template/.aioson/genomes/copywriting.meta.json +48 -0
- package/template/.aioson/git-guard.json +11 -0
- package/template/.aioson/mcp/servers.md +0 -1
- package/template/.aioson/rules/agent-language-policy.md +93 -0
- package/template/.aioson/rules/aioson-context-boundary.md +63 -0
- package/template/.aioson/rules/canonical-path-contract.md +47 -0
- package/template/.aioson/rules/data-format-convention.md +24 -86
- package/template/.aioson/rules/disk-first-artifacts.md +44 -0
- package/template/.aioson/rules/output-brevity.md +44 -0
- package/template/.aioson/rules/prd-section-ownership.md +49 -0
- package/template/.aioson/rules/security-baseline.md +139 -0
- package/template/.aioson/rules/spec-level-ownership.md +61 -0
- package/template/.aioson/rules/squad-driver-pattern.md +81 -0
- package/template/.aioson/schemas/squad-blueprint.schema.json +24 -0
- package/template/.aioson/schemas/squad-manifest.schema.json +44 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/pm.md +30 -0
- package/template/.aioson/skills/process/secure-tdd/SKILL.md +97 -0
- package/template/.aioson/skills/process/secure-tdd/references/nextjs.md +81 -0
- package/template/.aioson/skills/process/secure-tdd/references/node-express.md +91 -0
- package/template/.aioson/skills/process/secure-tdd/references/planned-stacks.md +33 -0
- package/template/.aioson/skills/static/harness-validate/SKILL.md +46 -0
- package/template/.aioson/skills/static/web-research-cache.md +3 -0
- package/template/.aioson/tasks/squad-create.md +35 -8
- package/template/.aioson/tasks/squad-design.md +50 -2
- package/template/.aioson/tasks/squad-investigate.md +14 -1
- package/template/.claude/commands/aioson/agent/committer.md +5 -0
- package/template/.claude/commands/aioson/agent/copywriter.md +5 -0
- package/template/.claude/commands/aioson/agent/cypher.md +5 -0
- package/template/.claude/commands/aioson/agent/pair.md +5 -0
- package/template/.claude/commands/aioson/agent/validator.md +5 -0
- package/template/.gemini/commands/aios-analyst.toml +6 -3
- package/template/.gemini/commands/aios-architect.toml +7 -6
- package/template/.gemini/commands/aios-committer.toml +7 -0
- package/template/.gemini/commands/aios-copywriter.toml +7 -0
- package/template/.gemini/commands/aios-cypher.toml +7 -0
- package/template/.gemini/commands/aios-dev.toml +8 -7
- package/template/.gemini/commands/aios-deyvin.toml +6 -5
- package/template/.gemini/commands/aios-discovery-design-doc.toml +6 -3
- package/template/.gemini/commands/aios-genome.toml +7 -0
- package/template/.gemini/commands/aios-neo.toml +5 -3
- package/template/.gemini/commands/aios-orache.toml +7 -0
- package/template/.gemini/commands/aios-orchestrator.toml +8 -7
- package/template/.gemini/commands/aios-pair.toml +6 -5
- package/template/.gemini/commands/aios-pm.toml +8 -7
- package/template/.gemini/commands/aios-product.toml +5 -3
- package/template/.gemini/commands/aios-qa.toml +6 -5
- package/template/.gemini/commands/aios-setup.toml +5 -2
- package/template/.gemini/commands/aios-sheldon.toml +7 -0
- package/template/.gemini/commands/aios-site-forge.toml +7 -0
- package/template/.gemini/commands/aios-squad.toml +7 -0
- package/template/.gemini/commands/aios-tester.toml +6 -5
- package/template/.gemini/commands/aios-ux-ui.toml +8 -7
- package/template/.gemini/commands/aios-validator.toml +7 -0
- package/template/AGENTS.md +12 -1
- package/template/CLAUDE.md +5 -1
- package/template/.aioson/locales/en/agents/analyst.md +0 -244
- package/template/.aioson/locales/en/agents/architect.md +0 -245
- package/template/.aioson/locales/en/agents/dev.md +0 -397
- package/template/.aioson/locales/en/agents/deyvin.md +0 -137
- package/template/.aioson/locales/en/agents/discovery-design-doc.md +0 -27
- package/template/.aioson/locales/en/agents/genome.md +0 -212
- package/template/.aioson/locales/en/agents/neo.md +0 -8
- package/template/.aioson/locales/en/agents/orache.md +0 -6
- package/template/.aioson/locales/en/agents/orchestrator.md +0 -189
- package/template/.aioson/locales/en/agents/pair.md +0 -5
- package/template/.aioson/locales/en/agents/pm.md +0 -84
- package/template/.aioson/locales/en/agents/product.md +0 -378
- package/template/.aioson/locales/en/agents/profiler-enricher.md +0 -5
- package/template/.aioson/locales/en/agents/profiler-forge.md +0 -5
- package/template/.aioson/locales/en/agents/profiler-researcher.md +0 -5
- package/template/.aioson/locales/en/agents/qa.md +0 -270
- package/template/.aioson/locales/en/agents/setup.md +0 -421
- package/template/.aioson/locales/en/agents/sheldon.md +0 -455
- package/template/.aioson/locales/en/agents/squad.md +0 -449
- package/template/.aioson/locales/en/agents/tester.md +0 -6
- package/template/.aioson/locales/en/agents/ux-ui.md +0 -668
- package/template/.aioson/locales/es/agents/analyst.md +0 -225
- package/template/.aioson/locales/es/agents/architect.md +0 -245
- package/template/.aioson/locales/es/agents/dev.md +0 -370
- package/template/.aioson/locales/es/agents/deyvin.md +0 -99
- package/template/.aioson/locales/es/agents/discovery-design-doc.md +0 -21
- package/template/.aioson/locales/es/agents/genome.md +0 -104
- package/template/.aioson/locales/es/agents/neo.md +0 -50
- package/template/.aioson/locales/es/agents/orache.md +0 -105
- package/template/.aioson/locales/es/agents/orchestrator.md +0 -194
- package/template/.aioson/locales/es/agents/pair.md +0 -7
- package/template/.aioson/locales/es/agents/pm.md +0 -90
- package/template/.aioson/locales/es/agents/product.md +0 -372
- package/template/.aioson/locales/es/agents/profiler-enricher.md +0 -7
- package/template/.aioson/locales/es/agents/profiler-forge.md +0 -7
- package/template/.aioson/locales/es/agents/profiler-researcher.md +0 -7
- package/template/.aioson/locales/es/agents/qa.md +0 -198
- package/template/.aioson/locales/es/agents/setup.md +0 -405
- package/template/.aioson/locales/es/agents/sheldon.md +0 -309
- package/template/.aioson/locales/es/agents/squad.md +0 -532
- package/template/.aioson/locales/es/agents/tester.md +0 -9
- package/template/.aioson/locales/es/agents/ux-ui.md +0 -212
- package/template/.aioson/locales/fr/agents/analyst.md +0 -225
- package/template/.aioson/locales/fr/agents/architect.md +0 -245
- package/template/.aioson/locales/fr/agents/dev.md +0 -370
- package/template/.aioson/locales/fr/agents/deyvin.md +0 -99
- package/template/.aioson/locales/fr/agents/discovery-design-doc.md +0 -21
- package/template/.aioson/locales/fr/agents/genome.md +0 -104
- package/template/.aioson/locales/fr/agents/neo.md +0 -50
- package/template/.aioson/locales/fr/agents/orache.md +0 -106
- package/template/.aioson/locales/fr/agents/orchestrator.md +0 -194
- package/template/.aioson/locales/fr/agents/pair.md +0 -7
- package/template/.aioson/locales/fr/agents/pm.md +0 -90
- package/template/.aioson/locales/fr/agents/product.md +0 -372
- package/template/.aioson/locales/fr/agents/profiler-enricher.md +0 -7
- package/template/.aioson/locales/fr/agents/profiler-forge.md +0 -7
- package/template/.aioson/locales/fr/agents/profiler-researcher.md +0 -7
- package/template/.aioson/locales/fr/agents/qa.md +0 -198
- package/template/.aioson/locales/fr/agents/setup.md +0 -405
- package/template/.aioson/locales/fr/agents/sheldon.md +0 -309
- package/template/.aioson/locales/fr/agents/squad.md +0 -532
- package/template/.aioson/locales/fr/agents/tester.md +0 -9
- package/template/.aioson/locales/fr/agents/ux-ui.md +0 -212
- package/template/.aioson/locales/pt-BR/agents/analyst.md +0 -319
- package/template/.aioson/locales/pt-BR/agents/architect.md +0 -284
- package/template/.aioson/locales/pt-BR/agents/dev.md +0 -483
- package/template/.aioson/locales/pt-BR/agents/deyvin.md +0 -184
- package/template/.aioson/locales/pt-BR/agents/discovery-design-doc.md +0 -198
- package/template/.aioson/locales/pt-BR/agents/genome.md +0 -297
- package/template/.aioson/locales/pt-BR/agents/neo.md +0 -208
- package/template/.aioson/locales/pt-BR/agents/orache.md +0 -137
- package/template/.aioson/locales/pt-BR/agents/orchestrator.md +0 -324
- package/template/.aioson/locales/pt-BR/agents/pair.md +0 -5
- package/template/.aioson/locales/pt-BR/agents/pm.md +0 -182
- package/template/.aioson/locales/pt-BR/agents/product.md +0 -466
- package/template/.aioson/locales/pt-BR/agents/profiler-enricher.md +0 -5
- package/template/.aioson/locales/pt-BR/agents/profiler-forge.md +0 -5
- package/template/.aioson/locales/pt-BR/agents/profiler-researcher.md +0 -5
- package/template/.aioson/locales/pt-BR/agents/qa.md +0 -300
- package/template/.aioson/locales/pt-BR/agents/setup.md +0 -533
- package/template/.aioson/locales/pt-BR/agents/sheldon.md +0 -323
- package/template/.aioson/locales/pt-BR/agents/squad.md +0 -1330
- package/template/.aioson/locales/pt-BR/agents/tester.md +0 -449
- package/template/.aioson/locales/pt-BR/agents/ux-ui.md +0 -669
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs/promises');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const LOCK_FILENAME = '.dossier.lock';
|
|
7
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
8
|
+
const DEFAULT_POLL_INTERVAL_MS = 200;
|
|
9
|
+
const DEFAULT_STALE_TTL_MS = 60_000;
|
|
10
|
+
|
|
11
|
+
function defaultIsAlive(pid) {
|
|
12
|
+
if (typeof pid !== 'number' || !Number.isFinite(pid)) return false;
|
|
13
|
+
try {
|
|
14
|
+
process.kill(pid, 0);
|
|
15
|
+
return true;
|
|
16
|
+
} catch (err) {
|
|
17
|
+
return err && err.code === 'EPERM';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sleep(ms) {
|
|
22
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isStaleLock(payload, { staleTtlMs, now, isAlive }) {
|
|
26
|
+
if (!payload || typeof payload !== 'object') return true;
|
|
27
|
+
const acquiredMs = Date.parse(payload.acquired_at);
|
|
28
|
+
if (!Number.isFinite(acquiredMs)) return true;
|
|
29
|
+
if (now() - acquiredMs >= staleTtlMs) return true;
|
|
30
|
+
if (typeof payload.pid !== 'number' || !isAlive(payload.pid)) return true;
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function acquireLock(lockDir, section, opts = {}) {
|
|
35
|
+
if (typeof lockDir !== 'string' || !lockDir) {
|
|
36
|
+
throw new TypeError('acquireLock: lockDir must be a non-empty string');
|
|
37
|
+
}
|
|
38
|
+
if (typeof section !== 'string' || !section) {
|
|
39
|
+
throw new TypeError('acquireLock: section must be a non-empty string');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const {
|
|
43
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
44
|
+
pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
|
|
45
|
+
staleTtlMs = DEFAULT_STALE_TTL_MS,
|
|
46
|
+
now = Date.now,
|
|
47
|
+
pid = process.pid,
|
|
48
|
+
isAlive = defaultIsAlive,
|
|
49
|
+
onWarn = (msg) => process.emitWarning(msg, 'DossierLockStale'),
|
|
50
|
+
sleepFn = sleep
|
|
51
|
+
} = opts;
|
|
52
|
+
|
|
53
|
+
const lockPath = path.join(lockDir, LOCK_FILENAME);
|
|
54
|
+
const start = now();
|
|
55
|
+
|
|
56
|
+
await fs.mkdir(lockDir, { recursive: true });
|
|
57
|
+
|
|
58
|
+
while (true) {
|
|
59
|
+
try {
|
|
60
|
+
const payload = {
|
|
61
|
+
pid,
|
|
62
|
+
section,
|
|
63
|
+
acquired_at: new Date(now()).toISOString()
|
|
64
|
+
};
|
|
65
|
+
const fh = await fs.open(lockPath, 'wx');
|
|
66
|
+
try {
|
|
67
|
+
await fh.writeFile(JSON.stringify(payload));
|
|
68
|
+
} finally {
|
|
69
|
+
await fh.close();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let released = false;
|
|
73
|
+
return async function release() {
|
|
74
|
+
if (released) return;
|
|
75
|
+
released = true;
|
|
76
|
+
try {
|
|
77
|
+
await fs.unlink(lockPath);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
if (err && err.code !== 'ENOENT') throw err;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
} catch (err) {
|
|
83
|
+
if (!err || err.code !== 'EEXIST') throw err;
|
|
84
|
+
|
|
85
|
+
let existing = null;
|
|
86
|
+
try {
|
|
87
|
+
const raw = await fs.readFile(lockPath, 'utf8');
|
|
88
|
+
existing = JSON.parse(raw);
|
|
89
|
+
} catch (_) {
|
|
90
|
+
existing = null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (isStaleLock(existing, { staleTtlMs, now, isAlive })) {
|
|
94
|
+
onWarn(
|
|
95
|
+
`stale dossier lockfile at ${lockPath} (pid=${existing && existing.pid != null ? existing.pid : '?'}); overriding`
|
|
96
|
+
);
|
|
97
|
+
try {
|
|
98
|
+
await fs.unlink(lockPath);
|
|
99
|
+
} catch (unlinkErr) {
|
|
100
|
+
if (unlinkErr && unlinkErr.code !== 'ENOENT') throw unlinkErr;
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (now() - start >= timeoutMs) {
|
|
106
|
+
const timeoutErr = new Error(
|
|
107
|
+
`timed out acquiring dossier lock at ${lockPath} after ${timeoutMs}ms`
|
|
108
|
+
);
|
|
109
|
+
timeoutErr.code = 'EDOSSIERLOCK';
|
|
110
|
+
timeoutErr.lockPath = lockPath;
|
|
111
|
+
timeoutErr.holder = existing;
|
|
112
|
+
throw timeoutErr;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
await sleepFn(pollIntervalMs);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
LOCK_FILENAME,
|
|
122
|
+
DEFAULT_TIMEOUT_MS,
|
|
123
|
+
DEFAULT_POLL_INTERVAL_MS,
|
|
124
|
+
DEFAULT_STALE_TTL_MS,
|
|
125
|
+
acquireLock,
|
|
126
|
+
isStaleLock,
|
|
127
|
+
defaultIsAlive
|
|
128
|
+
};
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs/promises');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
const { isCanonicalAgent, isValidSlug } = require('./schema');
|
|
8
|
+
const { shouldCompact, compact } = require('./dossier-compact');
|
|
9
|
+
|
|
10
|
+
const FEATURES_SUBDIR = 'features';
|
|
11
|
+
const REVISIONS_FILENAME = 'revisions.json';
|
|
12
|
+
const MAX_REVISION_ROUNDS = 3;
|
|
13
|
+
|
|
14
|
+
const VALID_SEVERITIES = new Set(['blocking', 'advisory']);
|
|
15
|
+
const VALID_STATUSES = new Set(['pending', 'approved', 'rejected', 'resolved']);
|
|
16
|
+
|
|
17
|
+
// Which workflow gate each agent "owns" (for anti-loop counter)
|
|
18
|
+
const GATE_BY_AGENT = {
|
|
19
|
+
product: 'requirements',
|
|
20
|
+
analyst: 'requirements',
|
|
21
|
+
architect: 'design',
|
|
22
|
+
'ux-ui': 'design',
|
|
23
|
+
pm: 'plan',
|
|
24
|
+
dev: 'plan',
|
|
25
|
+
qa: 'execution'
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function revisionsPath(contextDir, slug) {
|
|
29
|
+
return path.join(contextDir, FEATURES_SUBDIR, slug, REVISIONS_FILENAME);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function dossierPath(contextDir, slug) {
|
|
33
|
+
return path.join(contextDir, FEATURES_SUBDIR, slug, 'dossier.md');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function workflowStatePath(contextDir) {
|
|
37
|
+
return path.join(contextDir, 'workflow.state.json');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function nextRevId(existing) {
|
|
41
|
+
const n = existing.length + 1;
|
|
42
|
+
return `rev-${String(n).padStart(3, '0')}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function readRevisions({ slug, contextDir }) {
|
|
46
|
+
const p = revisionsPath(contextDir, slug);
|
|
47
|
+
try {
|
|
48
|
+
const raw = await fs.readFile(p, 'utf8');
|
|
49
|
+
const parsed = JSON.parse(raw);
|
|
50
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
51
|
+
} catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function writeRevisions({ slug, contextDir, revisions }) {
|
|
57
|
+
const p = revisionsPath(contextDir, slug);
|
|
58
|
+
await fs.mkdir(path.dirname(p), { recursive: true });
|
|
59
|
+
await fs.writeFile(p, JSON.stringify(revisions, null, 2) + '\n', 'utf8');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Replace the content of one ## section in a markdown file.
|
|
63
|
+
async function updateDossierSection(filePath, sectionName, newContent) {
|
|
64
|
+
let raw;
|
|
65
|
+
try {
|
|
66
|
+
raw = await fs.readFile(filePath, 'utf8');
|
|
67
|
+
} catch {
|
|
68
|
+
return; // dossier absent — skip silently
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const lines = raw.split('\n');
|
|
72
|
+
let sectionStart = -1;
|
|
73
|
+
let sectionEnd = lines.length;
|
|
74
|
+
|
|
75
|
+
for (let i = 0; i < lines.length; i++) {
|
|
76
|
+
if (lines[i].trimEnd() === `## ${sectionName}`) {
|
|
77
|
+
sectionStart = i;
|
|
78
|
+
} else if (sectionStart !== -1 && i > sectionStart && /^## /.test(lines[i])) {
|
|
79
|
+
sectionEnd = i;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let rebuilt;
|
|
85
|
+
if (sectionStart === -1) {
|
|
86
|
+
// Append section at end
|
|
87
|
+
rebuilt = raw.trimEnd() + `\n\n## ${sectionName}\n\n${newContent.trimEnd()}\n`;
|
|
88
|
+
} else {
|
|
89
|
+
const before = lines.slice(0, sectionStart + 1);
|
|
90
|
+
const after = lines.slice(sectionEnd);
|
|
91
|
+
rebuilt = [...before, '', newContent.trimEnd(), '', ...after].join('\n');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await fs.writeFile(filePath, rebuilt, 'utf8');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildRevisionTable(revisions) {
|
|
98
|
+
if (revisions.length === 0) {
|
|
99
|
+
return '_(sem revisões abertas)_';
|
|
100
|
+
}
|
|
101
|
+
const header = '| id | requested_by | target | severity | status |';
|
|
102
|
+
const divider = '|----|-------------|--------|----------|--------|';
|
|
103
|
+
const rows = revisions.map((r) =>
|
|
104
|
+
`| ${r.id} | ${r.requested_by} | ${r.target} | ${r.severity} | ${r.status} |`
|
|
105
|
+
);
|
|
106
|
+
return [header, divider, ...rows].join('\n');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function readWorkflowState(contextDir) {
|
|
110
|
+
const p = workflowStatePath(contextDir);
|
|
111
|
+
try {
|
|
112
|
+
const raw = await fs.readFile(p, 'utf8');
|
|
113
|
+
return JSON.parse(raw);
|
|
114
|
+
} catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function writeWorkflowState(contextDir, state) {
|
|
120
|
+
const p = workflowStatePath(contextDir);
|
|
121
|
+
await fs.writeFile(p, JSON.stringify(state, null, 2) + '\n', 'utf8');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function open({
|
|
125
|
+
slug,
|
|
126
|
+
contextDir,
|
|
127
|
+
requestedBy,
|
|
128
|
+
target,
|
|
129
|
+
targetArtifact,
|
|
130
|
+
reason,
|
|
131
|
+
severity,
|
|
132
|
+
evidenceCodeRefs = [],
|
|
133
|
+
now = () => new Date()
|
|
134
|
+
} = {}) {
|
|
135
|
+
if (!isValidSlug(slug)) {
|
|
136
|
+
const err = new Error(`invalid slug: ${JSON.stringify(slug)}`);
|
|
137
|
+
err.code = 'EREVSLUG';
|
|
138
|
+
throw err;
|
|
139
|
+
}
|
|
140
|
+
if (!isCanonicalAgent(requestedBy)) {
|
|
141
|
+
const err = new Error(`requested_by must be a canonical agent id (got: ${JSON.stringify(requestedBy)})`);
|
|
142
|
+
err.code = 'EREVAGENT';
|
|
143
|
+
throw err;
|
|
144
|
+
}
|
|
145
|
+
if (!isCanonicalAgent(target)) {
|
|
146
|
+
const err = new Error(`target must be a canonical agent id (got: ${JSON.stringify(target)})`);
|
|
147
|
+
err.code = 'EREVAGENT';
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
150
|
+
if (!VALID_SEVERITIES.has(severity)) {
|
|
151
|
+
const err = new Error(`severity must be 'blocking' or 'advisory' (got: ${JSON.stringify(severity)})`);
|
|
152
|
+
err.code = 'EREVSCHEMA';
|
|
153
|
+
throw err;
|
|
154
|
+
}
|
|
155
|
+
if (!targetArtifact || typeof targetArtifact !== 'string') {
|
|
156
|
+
const err = new Error('target_artifact must be a non-empty relative path');
|
|
157
|
+
err.code = 'EREVSCHEMA';
|
|
158
|
+
throw err;
|
|
159
|
+
}
|
|
160
|
+
if (!reason || typeof reason !== 'string' || !reason.trim()) {
|
|
161
|
+
const err = new Error('reason must be a non-empty string');
|
|
162
|
+
err.code = 'EREVSCHEMA';
|
|
163
|
+
throw err;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const revisions = await readRevisions({ slug, contextDir });
|
|
167
|
+
const id = nextRevId(revisions);
|
|
168
|
+
const createdAt = now().toISOString();
|
|
169
|
+
|
|
170
|
+
const revision = {
|
|
171
|
+
id,
|
|
172
|
+
status: 'pending',
|
|
173
|
+
requested_by: requestedBy,
|
|
174
|
+
target,
|
|
175
|
+
target_artifact: targetArtifact,
|
|
176
|
+
reason: reason.trim(),
|
|
177
|
+
severity,
|
|
178
|
+
evidence_code_refs: Array.isArray(evidenceCodeRefs) ? evidenceCodeRefs : [],
|
|
179
|
+
created_at: createdAt,
|
|
180
|
+
resolved_at: null,
|
|
181
|
+
resolution: null
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
revisions.push(revision);
|
|
185
|
+
await writeRevisions({ slug, contextDir, revisions });
|
|
186
|
+
|
|
187
|
+
// Update dossier ## Revision Requests
|
|
188
|
+
const dp = dossierPath(contextDir, slug);
|
|
189
|
+
await updateDossierSection(dp, 'Revision Requests', buildRevisionTable(revisions));
|
|
190
|
+
|
|
191
|
+
return revision;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function list({ slug, contextDir, status } = {}) {
|
|
195
|
+
if (!isValidSlug(slug)) {
|
|
196
|
+
const err = new Error(`invalid slug: ${JSON.stringify(slug)}`);
|
|
197
|
+
err.code = 'EREVSLUG';
|
|
198
|
+
throw err;
|
|
199
|
+
}
|
|
200
|
+
const revisions = await readRevisions({ slug, contextDir });
|
|
201
|
+
if (status && VALID_STATUSES.has(status)) {
|
|
202
|
+
return revisions.filter((r) => r.status === status);
|
|
203
|
+
}
|
|
204
|
+
return revisions;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function resolve({
|
|
208
|
+
slug,
|
|
209
|
+
contextDir,
|
|
210
|
+
revId,
|
|
211
|
+
action,
|
|
212
|
+
forceRevision = false,
|
|
213
|
+
now = () => new Date()
|
|
214
|
+
} = {}) {
|
|
215
|
+
if (!isValidSlug(slug)) {
|
|
216
|
+
const err = new Error(`invalid slug: ${JSON.stringify(slug)}`);
|
|
217
|
+
err.code = 'EREVSLUG';
|
|
218
|
+
throw err;
|
|
219
|
+
}
|
|
220
|
+
if (action !== 'approve' && action !== 'reject') {
|
|
221
|
+
const err = new Error(`action must be 'approve' or 'reject' (got: ${JSON.stringify(action)})`);
|
|
222
|
+
err.code = 'EREVSCHEMA';
|
|
223
|
+
throw err;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const revisions = await readRevisions({ slug, contextDir });
|
|
227
|
+
const idx = revisions.findIndex((r) => r.id === revId);
|
|
228
|
+
if (idx === -1) {
|
|
229
|
+
const err = new Error(`revision '${revId}' not found in ${slug}`);
|
|
230
|
+
err.code = 'EREVNOTFOUND';
|
|
231
|
+
throw err;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const rev = revisions[idx];
|
|
235
|
+
if (rev.status !== 'pending') {
|
|
236
|
+
const err = new Error(`revision '${revId}' is '${rev.status}' — only pending revisions can be resolved`);
|
|
237
|
+
err.code = 'EREVNOTPENDING';
|
|
238
|
+
throw err;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const resolvedAt = now().toISOString();
|
|
242
|
+
|
|
243
|
+
if (action === 'reject') {
|
|
244
|
+
revisions[idx] = { ...rev, status: 'rejected', resolved_at: resolvedAt, resolution: 'rejected' };
|
|
245
|
+
await writeRevisions({ slug, contextDir, revisions });
|
|
246
|
+
const dp = dossierPath(contextDir, slug);
|
|
247
|
+
await updateDossierSection(dp, 'Revision Requests', buildRevisionTable(revisions));
|
|
248
|
+
if (await shouldCompact({ slug, contextDir })) await compact({ slug, contextDir });
|
|
249
|
+
return { revision: revisions[idx], gateIncremented: null };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// approve path: check anti-loop limit
|
|
253
|
+
const gate = GATE_BY_AGENT[rev.target] || null;
|
|
254
|
+
let gateIncremented = null;
|
|
255
|
+
let newRounds = null;
|
|
256
|
+
|
|
257
|
+
if (gate) {
|
|
258
|
+
const wfState = await readWorkflowState(contextDir);
|
|
259
|
+
if (wfState) {
|
|
260
|
+
const rounds = (wfState.gate_revision_rounds || {})[gate] || 0;
|
|
261
|
+
if (rounds >= MAX_REVISION_ROUNDS && !forceRevision) {
|
|
262
|
+
const err = new Error(
|
|
263
|
+
`Anti-loop: gate '${gate}' already had ${rounds} revision round(s) (max ${MAX_REVISION_ROUNDS}). ` +
|
|
264
|
+
`Use --force-revision to override.`
|
|
265
|
+
);
|
|
266
|
+
err.code = 'EREVLOOP';
|
|
267
|
+
err.gate = gate;
|
|
268
|
+
err.rounds = rounds;
|
|
269
|
+
throw err;
|
|
270
|
+
}
|
|
271
|
+
newRounds = rounds + 1;
|
|
272
|
+
const updatedState = {
|
|
273
|
+
...wfState,
|
|
274
|
+
gate_revision_rounds: {
|
|
275
|
+
...(wfState.gate_revision_rounds || {}),
|
|
276
|
+
[gate]: newRounds
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
await writeWorkflowState(contextDir, updatedState);
|
|
280
|
+
gateIncremented = { gate, rounds: newRounds };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
revisions[idx] = { ...rev, status: 'approved', resolved_at: resolvedAt, resolution: 'approved' };
|
|
285
|
+
await writeRevisions({ slug, contextDir, revisions });
|
|
286
|
+
const dp = dossierPath(contextDir, slug);
|
|
287
|
+
await updateDossierSection(dp, 'Revision Requests', buildRevisionTable(revisions));
|
|
288
|
+
if (await shouldCompact({ slug, contextDir })) await compact({ slug, contextDir });
|
|
289
|
+
|
|
290
|
+
return { revision: revisions[idx], gateIncremented };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function getBlockingRevisions({ slug, contextDir }) {
|
|
294
|
+
if (!slug || !isValidSlug(slug)) return [];
|
|
295
|
+
const revisions = await readRevisions({ slug, contextDir });
|
|
296
|
+
return revisions.filter((r) => r.severity === 'blocking' && r.status === 'pending');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
module.exports = {
|
|
300
|
+
VALID_SEVERITIES,
|
|
301
|
+
VALID_STATUSES,
|
|
302
|
+
GATE_BY_AGENT,
|
|
303
|
+
MAX_REVISION_ROUNDS,
|
|
304
|
+
revisionsPath,
|
|
305
|
+
readRevisions,
|
|
306
|
+
writeRevisions,
|
|
307
|
+
updateDossierSection,
|
|
308
|
+
buildRevisionTable,
|
|
309
|
+
open,
|
|
310
|
+
list,
|
|
311
|
+
resolve,
|
|
312
|
+
getBlockingRevisions
|
|
313
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const SCHEMA_VERSION = '1.0';
|
|
4
|
+
|
|
5
|
+
const CANONICAL_AGENT_IDS = Object.freeze(new Set([
|
|
6
|
+
'analyst',
|
|
7
|
+
'architect',
|
|
8
|
+
'committer',
|
|
9
|
+
'copywriter',
|
|
10
|
+
'cypher',
|
|
11
|
+
'design-hybrid-forge',
|
|
12
|
+
'dev',
|
|
13
|
+
'deyvin',
|
|
14
|
+
'discover',
|
|
15
|
+
'discovery-design-doc',
|
|
16
|
+
'genome',
|
|
17
|
+
'neo',
|
|
18
|
+
'orache',
|
|
19
|
+
'orchestrator',
|
|
20
|
+
'pair',
|
|
21
|
+
'pentester',
|
|
22
|
+
'pm',
|
|
23
|
+
'product',
|
|
24
|
+
'profiler-enricher',
|
|
25
|
+
'profiler-forge',
|
|
26
|
+
'profiler-researcher',
|
|
27
|
+
'qa',
|
|
28
|
+
'setup',
|
|
29
|
+
'sheldon',
|
|
30
|
+
'site-forge',
|
|
31
|
+
'squad',
|
|
32
|
+
'tester',
|
|
33
|
+
'ux-ui',
|
|
34
|
+
'validator'
|
|
35
|
+
]));
|
|
36
|
+
|
|
37
|
+
const ORIGIN_PSEUDO_IDS = Object.freeze(new Set(['dossier-init', 'dossier-init-prompt']));
|
|
38
|
+
|
|
39
|
+
const REQUIRED_FRONTMATTER_FIELDS = Object.freeze([
|
|
40
|
+
'feature_slug',
|
|
41
|
+
'schema_version',
|
|
42
|
+
'created_by',
|
|
43
|
+
'created_at',
|
|
44
|
+
'status',
|
|
45
|
+
'classification',
|
|
46
|
+
'last_updated_by',
|
|
47
|
+
'last_updated_at'
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
const ALLOWED_STATUSES = Object.freeze(new Set(['active', 'paused', 'closed']));
|
|
51
|
+
const ALLOWED_CLASSIFICATIONS = Object.freeze(new Set(['MICRO', 'SMALL', 'MEDIUM']));
|
|
52
|
+
|
|
53
|
+
const REQUIRED_SECTIONS = Object.freeze([
|
|
54
|
+
'Why',
|
|
55
|
+
'What',
|
|
56
|
+
'Code Map',
|
|
57
|
+
'Rules & Design-Docs aplicáveis',
|
|
58
|
+
'Agent Trail',
|
|
59
|
+
'Revision Requests'
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
const SLUG_REGEX = /^[a-z0-9][a-z0-9-]*$/;
|
|
63
|
+
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
64
|
+
|
|
65
|
+
function isValidSlug(value) {
|
|
66
|
+
return typeof value === 'string' && SLUG_REGEX.test(value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isValidIsoDate(value) {
|
|
70
|
+
if (typeof value !== 'string' || !ISO_DATE_REGEX.test(value)) return false;
|
|
71
|
+
const ts = Date.parse(value);
|
|
72
|
+
return Number.isFinite(ts);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isCanonicalAgent(value) {
|
|
76
|
+
return typeof value === 'string' && CANONICAL_AGENT_IDS.has(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isAllowedAuthor(value) {
|
|
80
|
+
return isCanonicalAgent(value) || (typeof value === 'string' && ORIGIN_PSEUDO_IDS.has(value));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function validateFrontmatter(fm) {
|
|
84
|
+
const errors = [];
|
|
85
|
+
|
|
86
|
+
if (!fm || typeof fm !== 'object') {
|
|
87
|
+
return { valid: false, errors: ['frontmatter is missing or not an object'] };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (const field of REQUIRED_FRONTMATTER_FIELDS) {
|
|
91
|
+
if (!Object.prototype.hasOwnProperty.call(fm, field) || fm[field] === '' || fm[field] === null || fm[field] === undefined) {
|
|
92
|
+
errors.push(`missing required field: ${field}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (fm.feature_slug !== undefined && !isValidSlug(fm.feature_slug)) {
|
|
97
|
+
errors.push(`feature_slug must be kebab-case (got: ${JSON.stringify(fm.feature_slug)})`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (fm.schema_version !== undefined && fm.schema_version !== SCHEMA_VERSION) {
|
|
101
|
+
errors.push(`unsupported schema_version: ${JSON.stringify(fm.schema_version)} (expected ${SCHEMA_VERSION})`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (fm.status !== undefined && !ALLOWED_STATUSES.has(fm.status)) {
|
|
105
|
+
errors.push(`status must be one of [${[...ALLOWED_STATUSES].join(', ')}] (got: ${JSON.stringify(fm.status)})`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (fm.classification !== undefined && !ALLOWED_CLASSIFICATIONS.has(fm.classification)) {
|
|
109
|
+
errors.push(`classification must be one of [${[...ALLOWED_CLASSIFICATIONS].join(', ')}] (got: ${JSON.stringify(fm.classification)})`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (fm.created_by !== undefined && !isAllowedAuthor(fm.created_by)) {
|
|
113
|
+
errors.push(`created_by must be a canonical agent id or 'dossier-init' (got: ${JSON.stringify(fm.created_by)})`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (fm.last_updated_by !== undefined && !isAllowedAuthor(fm.last_updated_by)) {
|
|
117
|
+
errors.push(`last_updated_by must be a canonical agent id or 'dossier-init' (got: ${JSON.stringify(fm.last_updated_by)})`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (fm.created_at !== undefined && !isValidIsoDate(fm.created_at)) {
|
|
121
|
+
errors.push(`created_at must be ISO 8601 (got: ${JSON.stringify(fm.created_at)})`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (fm.last_updated_at !== undefined && !isValidIsoDate(fm.last_updated_at)) {
|
|
125
|
+
errors.push(`last_updated_at must be ISO 8601 (got: ${JSON.stringify(fm.last_updated_at)})`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { valid: errors.length === 0, errors };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function assertFrontmatter(fm) {
|
|
132
|
+
const result = validateFrontmatter(fm);
|
|
133
|
+
if (!result.valid) {
|
|
134
|
+
const err = new Error(`invalid dossier frontmatter: ${result.errors.join('; ')}`);
|
|
135
|
+
err.code = 'EDOSSIERSCHEMA';
|
|
136
|
+
err.errors = result.errors;
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = {
|
|
142
|
+
SCHEMA_VERSION,
|
|
143
|
+
CANONICAL_AGENT_IDS,
|
|
144
|
+
ORIGIN_PSEUDO_IDS,
|
|
145
|
+
REQUIRED_FRONTMATTER_FIELDS,
|
|
146
|
+
ALLOWED_STATUSES,
|
|
147
|
+
ALLOWED_CLASSIFICATIONS,
|
|
148
|
+
REQUIRED_SECTIONS,
|
|
149
|
+
isValidSlug,
|
|
150
|
+
isValidIsoDate,
|
|
151
|
+
isCanonicalAgent,
|
|
152
|
+
isAllowedAuthor,
|
|
153
|
+
validateFrontmatter,
|
|
154
|
+
assertFrontmatter
|
|
155
|
+
};
|