@use-the-fork/spindle 0.0.0-stage → 0.0.2
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/LICENSE +21 -0
- package/README.md +3 -2
- package/build/Assets/Blueprints/Catalog.js +110 -0
- package/build/Assets/Blueprints/agents/general.agent.md +67 -0
- package/build/Assets/Blueprints/agents/sdd-code-reviewer.agent.md +62 -0
- package/build/Assets/Blueprints/agents/sdd-code-slop-reviewer.agent.md +59 -0
- package/build/Assets/Blueprints/agents/sdd-implementer.agent.md +77 -0
- package/build/Assets/Blueprints/agents/sdd-orchestrator.agent.md +42 -0
- package/build/Assets/Blueprints/agents/sdd-re-review.agent.md +57 -0
- package/build/Assets/Blueprints/agents/sdd-reviewer.agent.md +89 -0
- package/build/Assets/Blueprints/agents/sdd-task-reviewer.agent.md +54 -0
- package/build/Assets/Blueprints/agents/shared-codebase-analyzer.agent.md +151 -0
- package/build/Assets/Blueprints/agents/shared-codebase-locator.agent.md +130 -0
- package/build/Assets/Blueprints/agents/shared-codebase-pattern-finder.agent.md +247 -0
- package/build/Assets/Blueprints/agents/shared-explore.agent.md +53 -0
- package/build/Assets/Blueprints/agents/shared-r.agent.md +17 -0
- package/build/Assets/Blueprints/agents/shared-rw.agent.md +17 -0
- package/build/Assets/Blueprints/agents/shared-rwx.agent.md +17 -0
- package/build/Assets/Blueprints/agents/shared-web-search-researcher.agent.md +122 -0
- package/build/Assets/Blueprints/instructions/code-commenting.instructions.md +39 -0
- package/build/Assets/Blueprints/instructions/source-architecture.instructions.md +15 -0
- package/build/Assets/Blueprints/skills/asd-ste100/CREDITS.MD +1 -0
- package/build/Assets/Blueprints/skills/asd-ste100/SKILL.md +83 -0
- package/build/Assets/Blueprints/skills/asd-ste100/examples/before-after.md +107 -0
- package/build/Assets/Blueprints/skills/asd-ste100/references/writing-rules.md +65 -0
- package/build/Assets/Blueprints/skills/author-agent/SKILL.md +26 -0
- package/build/Assets/Blueprints/skills/author-design/SKILL.md +38 -0
- package/build/Assets/Blueprints/skills/author-instruction/SKILL.md +28 -0
- package/build/Assets/Blueprints/skills/author-skill/SKILL.md +26 -0
- package/build/Assets/Blueprints/skills/grill/SKILL.md +33 -0
- package/build/Assets/Blueprints/skills/grill-design/SKILL.md +41 -0
- package/build/Assets/Blueprints/skills/grill-with-docs/SKILL.md +14 -0
- package/build/Assets/Blueprints/skills/implement/SKILL.md +79 -0
- package/build/Assets/Blueprints/skills/implement-spec/SKILL.md +34 -0
- package/build/Assets/Blueprints/skills/improve-codebase-architecture/HTML-REPORT.md +129 -0
- package/build/Assets/Blueprints/skills/improve-codebase-architecture/SKILL.md +74 -0
- package/build/Assets/Blueprints/skills/int-code-review/SKILL.md +76 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/SKILL.md +238 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/_sections.md +36 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/_template.md +42 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/comments-closing-brace-labels.md +112 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/comments-empty-docblocks.md +132 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/comments-narration.md +118 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/comments-placeholder.md +130 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/defensive-generic-catch.md +153 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/defensive-impossible-null.md +157 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/defensive-missing-real.md +157 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/naming-generic-placeholders.md +120 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/naming-over-descriptive.md +108 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/naming-suffix-abuse.md +167 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/naming-type-in-name.md +100 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/over-eng-dependency-creep.md +114 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/over-eng-premature-interface.md +136 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/over-eng-single-method-class.md +161 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/over-eng-useless-wrapper.md +140 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/style-as-any-escape.md +127 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/style-debug-artifacts.md +144 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/style-hyper-consistent.md +84 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/style-no-hack-scars.md +113 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/style-trivial-boilerplate.md +191 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/test-doesnt-throw.md +132 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/test-mirror-implementation.md +125 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/test-mock-everything.md +148 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/test-snapshot-abuse.md +131 -0
- package/build/Assets/Blueprints/skills/int-codebase-design/SKILL.md +116 -0
- package/build/Assets/Blueprints/skills/int-codebase-design/references/DEEPENING.md +37 -0
- package/build/Assets/Blueprints/skills/int-codebase-design/references/DESIGN-IT-TWICE.md +44 -0
- package/build/Assets/Blueprints/skills/int-domain-modeling/SKILL.md +78 -0
- package/build/Assets/Blueprints/skills/int-domain-modeling/references/ADR-FORMAT.md +47 -0
- package/build/Assets/Blueprints/skills/int-domain-modeling/references/GLOSSARY-FORMAT.md +60 -0
- package/build/Assets/Blueprints/skills/int-prototype/LOGIC.md +67 -0
- package/build/Assets/Blueprints/skills/int-prototype/SKILL.md +30 -0
- package/build/Assets/Blueprints/skills/int-prototype/UI.md +112 -0
- package/build/Assets/Blueprints/skills/int-retrospective/SKILL.md +88 -0
- package/build/Assets/Blueprints/skills/int-retrospective/assets/retrospective-prompt.md +38 -0
- package/build/Assets/Blueprints/skills/int-retrospective/references/assessment.md +56 -0
- package/build/Assets/Blueprints/skills/int-subagent-driven-dev/SKILL.md +64 -0
- package/build/Assets/Blueprints/skills/int-subagent-driven-dev/assets/implementer-prompt.md +40 -0
- package/build/Assets/Blueprints/skills/int-subagent-driven-dev/assets/reviewer-prompt.md +43 -0
- package/build/Assets/Blueprints/skills/int-subagent-driven-dev/references/aggregate-review.md +7 -0
- package/build/Assets/Blueprints/skills/int-subagent-driven-dev/references/corrective-rounds.md +72 -0
- package/build/Assets/Blueprints/skills/int-subagent-driven-dev/references/example-workflow.md +25 -0
- package/build/Assets/Blueprints/skills/int-subagent-driven-dev/references/full-spec.md +72 -0
- package/build/Assets/Blueprints/skills/int-subagent-driven-dev/references/implementer-corrections.md +9 -0
- package/build/Assets/Blueprints/skills/int-subagent-driven-dev/references/review-inputs.md +26 -0
- package/build/Assets/Blueprints/skills/int-subagent-driven-dev/references/verification-evidence.md +7 -0
- package/build/Assets/Blueprints/skills/int-test-driven-development/SKILL.md +51 -0
- package/build/Assets/Blueprints/skills/int-test-driven-development/references/mocking.md +31 -0
- package/build/Assets/Blueprints/skills/int-test-driven-development/references/tests.md +68 -0
- package/build/Assets/Blueprints/skills/int-testing-agent-guidance/SKILL.md +53 -0
- package/build/Assets/Blueprints/skills/int-verification-before-completion/SKILL.md +25 -0
- package/build/Assets/Blueprints/skills/int-writing-agents/SKILL.md +66 -0
- package/build/Assets/Blueprints/skills/int-writing-for-agents/SKILL.md +86 -0
- package/build/Assets/Blueprints/skills/int-writing-instruction/SKILL.md +57 -0
- package/build/Assets/Blueprints/skills/int-writing-skills/SKILL.md +129 -0
- package/build/Assets/Blueprints/skills/int-writing-skills/assets/interactive.md +83 -0
- package/build/Assets/Blueprints/skills/int-writing-skills/assets/reference.md +32 -0
- package/build/Assets/Blueprints/skills/int-writing-skills/references/arguments.md +50 -0
- package/build/Assets/Blueprints/skills/int-writing-skills/references/interactive-skills.md +53 -0
- package/build/Assets/Blueprints/skills/int-writing-skills/references/persuasion-principles.md +220 -0
- package/build/Assets/Blueprints/skills/int-writing-skills/references/reference-skills.md +20 -0
- package/build/Assets/Blueprints/skills/int-writing-skills/references/testing-skills.md +48 -0
- package/build/Assets/Blueprints/skills/retrospective/SKILL.md +16 -0
- package/build/Assets/Blueprints/skills/setup-project/SKILL.md +66 -0
- package/build/Assets/Blueprints/skills/to-spec/SKILL.md +53 -0
- package/build/Assets/Blueprints/skills/to-tickets/SKILL.md +49 -0
- package/build/Assets/Blueprints/skills/update-tooling/SKILL.md +86 -0
- package/build/Assets/Templates/Catalog.js +12 -0
- package/build/Assets/Templates/Catalog.ts +17 -0
- package/build/Assets/Templates/project/copilot-instructions.md +17 -0
- package/build/Assets/Templates/project/domain.md +51 -0
- package/build/Assets/Templates/project/ticket-tracker.md +33 -0
- package/build/Assets/Templates/sdd/design.md +25 -0
- package/build/Assets/Templates/sdd/spec.md +29 -0
- package/build/Assets/Templates/sdd/ticket.md +15 -0
- package/build/BlueprintManagement/BlueprintManagementServiceProvider.js +15 -0
- package/build/BlueprintManagement/Catalog/BlueprintCatalog.js +238 -0
- package/build/BlueprintManagement/Commands/BlueprintCommandOptions.js +9 -0
- package/build/BlueprintManagement/Commands/ConflictPolicy.js +6 -0
- package/build/BlueprintManagement/Commands/InitCommand.js +117 -0
- package/build/BlueprintManagement/Commands/Prompts.js +66 -0
- package/build/BlueprintManagement/Commands/UpdateCommand.js +86 -0
- package/build/BlueprintManagement/Handlers/Agents/AgentHandler.js +21 -0
- package/build/BlueprintManagement/Handlers/BlueprintHandlers.js +4 -0
- package/build/BlueprintManagement/Handlers/Instructions/InstructionHandler.js +21 -0
- package/build/BlueprintManagement/Handlers/Skills/SkillHandler.js +10 -0
- package/build/BlueprintManagement/Reconciliation/BlueprintCatalogPlanner.js +87 -0
- package/build/BlueprintManagement/Reconciliation/BlueprintRemovalCoordinator.js +78 -0
- package/build/BlueprintManagement/Services/BlueprintReconciliationService.js +391 -0
- package/build/BlueprintManagement/Services/InstallationService.js +54 -0
- package/build/BlueprintManagement/Support/BlueprintFiles.js +113 -0
- package/build/BlueprintManagement/Types/Blueprint.js +1 -0
- package/build/BlueprintManagement/Types/Installation.js +1 -0
- package/build/BlueprintManagement/Types/Reconciliation.js +1 -0
- package/build/BlueprintManagement/Versioning/BlueprintVersion.js +70 -0
- package/build/BlueprintManagement/Versioning/BlueprintVersioning.js +55 -0
- package/build/Commands/Command.js +3 -0
- package/build/Commands/CommandOptions.js +12 -0
- package/build/Config/Config.js +3 -0
- package/build/Foundation/Application.js +112 -0
- package/build/Foundation/ApplicationBuilder.js +23 -0
- package/build/Foundation/Bootstrap/Bootstrapper.js +2 -0
- package/build/Foundation/Bootstrap/DiscoverGitRoot.js +8 -0
- package/build/Foundation/Bootstrap/LoadConsoleArgs.js +47 -0
- package/build/Foundation/Bootstrap/LoadProjectConfiguration.js +144 -0
- package/build/Foundation/Bootstrap/RegisterProviders.js +12 -0
- package/build/Foundation/Bootstrap/RequireProjectConfiguration.js +14 -0
- package/build/Foundation/ConsoleInvocation.js +1 -0
- package/build/Foundation/Container.js +185 -0
- package/build/Foundation/FoundationServiceProvider.js +5 -0
- package/build/Foundation/Kernel.js +30 -0
- package/build/Foundation/Service.js +23 -0
- package/build/Foundation/ServiceProvider.js +12 -0
- package/build/Git/GitServiceProvider.js +8 -0
- package/build/Git/Services/GitService.js +26 -0
- package/build/Project/ProjectServiceProvider.js +8 -0
- package/build/Project/Services/ProjectService.js +124 -0
- package/build/SubagentDrivenDevelopment/Commands/ChecksRecordCommand.js +35 -0
- package/build/SubagentDrivenDevelopment/Commands/ChecksRequireCommand.js +33 -0
- package/build/SubagentDrivenDevelopment/Commands/ClaimCommand.js +23 -0
- package/build/SubagentDrivenDevelopment/Commands/CorrectCommand.js +37 -0
- package/build/SubagentDrivenDevelopment/Commands/CreateCommand.js +26 -0
- package/build/SubagentDrivenDevelopment/Commands/FindingCreateCommand.js +39 -0
- package/build/SubagentDrivenDevelopment/Commands/FindingUpdateCommand.js +36 -0
- package/build/SubagentDrivenDevelopment/Commands/FinishCommand.js +23 -0
- package/build/SubagentDrivenDevelopment/Commands/ImplementCommand.js +23 -0
- package/build/SubagentDrivenDevelopment/Commands/NextCommand.js +21 -0
- package/build/SubagentDrivenDevelopment/Commands/ReadyCommand.js +21 -0
- package/build/SubagentDrivenDevelopment/Commands/RequirementsDecideCommand.js +33 -0
- package/build/SubagentDrivenDevelopment/Commands/ResumeCommand.js +31 -0
- package/build/SubagentDrivenDevelopment/Commands/ReviewCommand.js +55 -0
- package/build/SubagentDrivenDevelopment/Commands/ReviewRequireCommand.js +12 -0
- package/build/SubagentDrivenDevelopment/Commands/StatusCommand.js +21 -0
- package/build/SubagentDrivenDevelopment/Commands/SubmitCommand.js +59 -0
- package/build/SubagentDrivenDevelopment/Commands/TicketCommentCommand.js +24 -0
- package/build/SubagentDrivenDevelopment/Commands/TicketCreateCommand.js +25 -0
- package/build/SubagentDrivenDevelopment/Commands/TicketUpdateCommand.js +31 -0
- package/build/SubagentDrivenDevelopment/Commands/groups.js +36 -0
- package/build/SubagentDrivenDevelopment/Evidence/GitEvidence.js +449 -0
- package/build/SubagentDrivenDevelopment/Evidence/ReportEvidence.js +80 -0
- package/build/SubagentDrivenDevelopment/Models/Assignments.js +1 -0
- package/build/SubagentDrivenDevelopment/Models/Evidence.js +1 -0
- package/build/SubagentDrivenDevelopment/Models/Findings.js +1 -0
- package/build/SubagentDrivenDevelopment/Models/Options.js +1 -0
- package/build/SubagentDrivenDevelopment/Models/Workflow.js +16 -0
- package/build/SubagentDrivenDevelopment/Persistence/AssignmentSchema.js +155 -0
- package/build/SubagentDrivenDevelopment/Persistence/CheckSchema.js +71 -0
- package/build/SubagentDrivenDevelopment/Persistence/FeatureWorkspace.js +76 -0
- package/build/SubagentDrivenDevelopment/Persistence/FileSystem.js +28 -0
- package/build/SubagentDrivenDevelopment/Persistence/FindingSchema.js +81 -0
- package/build/SubagentDrivenDevelopment/Persistence/LedgerRepository.js +322 -0
- package/build/SubagentDrivenDevelopment/Persistence/PathSafety.js +62 -0
- package/build/SubagentDrivenDevelopment/Persistence/ProgressSchema.js +95 -0
- package/build/SubagentDrivenDevelopment/Persistence/ReadinessReader.js +40 -0
- package/build/SubagentDrivenDevelopment/Persistence/ReviewSchema.js +68 -0
- package/build/SubagentDrivenDevelopment/Persistence/TicketRepository.js +147 -0
- package/build/SubagentDrivenDevelopment/Persistence/WorkspaceTransaction.js +282 -0
- package/build/SubagentDrivenDevelopment/Policies/AcceptancePolicy.js +100 -0
- package/build/SubagentDrivenDevelopment/Policies/AssignmentPolicy.js +36 -0
- package/build/SubagentDrivenDevelopment/Policies/CheckPolicy.js +93 -0
- package/build/SubagentDrivenDevelopment/Policies/FindingPolicy.js +53 -0
- package/build/SubagentDrivenDevelopment/Policies/InputValidation.js +36 -0
- package/build/SubagentDrivenDevelopment/Policies/ReadinessPolicy.js +26 -0
- package/build/SubagentDrivenDevelopment/Policies/RequiredReviewers.js +12 -0
- package/build/SubagentDrivenDevelopment/Policies/ReviewPolicy.js +87 -0
- package/build/SubagentDrivenDevelopment/Policies/WorkflowPolicy.js +85 -0
- package/build/SubagentDrivenDevelopment/Services/AssignmentService.js +269 -0
- package/build/SubagentDrivenDevelopment/Services/ChecksService.js +319 -0
- package/build/SubagentDrivenDevelopment/Services/CorrectionService.js +273 -0
- package/build/SubagentDrivenDevelopment/Services/DeliveryCorrectionService.js +253 -0
- package/build/SubagentDrivenDevelopment/Services/DeliveryFindingService.js +302 -0
- package/build/SubagentDrivenDevelopment/Services/DeliveryService.js +160 -0
- package/build/SubagentDrivenDevelopment/Services/DeliverySubmissionService.js +273 -0
- package/build/SubagentDrivenDevelopment/Services/FeatureCreationService.js +138 -0
- package/build/SubagentDrivenDevelopment/Services/FeatureInspectionService.js +399 -0
- package/build/SubagentDrivenDevelopment/Services/FindingService.js +392 -0
- package/build/SubagentDrivenDevelopment/Services/IntegrationService.js +258 -0
- package/build/SubagentDrivenDevelopment/Services/ReadinessService.js +21 -0
- package/build/SubagentDrivenDevelopment/Services/RequirementsService.js +216 -0
- package/build/SubagentDrivenDevelopment/Services/ReviewService.js +471 -0
- package/build/SubagentDrivenDevelopment/Services/SubmissionService.js +299 -0
- package/build/SubagentDrivenDevelopment/Services/TicketService.js +186 -0
- package/build/SubagentDrivenDevelopment/SubagentDrivenDevelopmentServiceProvider.js +113 -0
- package/build/Support/Records.js +5 -0
- package/build/Support/Repository.js +139 -0
- package/build/Support/Stacks.js +10 -0
- package/build/TemplateRendering/Computed/ComputedContextService.js +13 -0
- package/build/TemplateRendering/Computed/ComputedVariableProvider.js +6 -0
- package/build/TemplateRendering/Computed/ComputedVariableProviders.js +6 -0
- package/build/TemplateRendering/Computed/Providers/InternalSkillDescriptionProvider.js +6 -0
- package/build/TemplateRendering/Computed/Providers/SpindlePackageManagerProvider.js +11 -0
- package/build/TemplateRendering/Computed/ResolveRenderContext.js +18 -0
- package/build/TemplateRendering/Rendering/RenderText.js +43 -0
- package/build/TemplateRendering/TemplateRenderingServiceProvider.js +8 -0
- package/build/Tooling/Commands/ToolingAddCommand.js +29 -0
- package/build/Tooling/Commands/ToolingListCommand.js +19 -0
- package/build/Tooling/Commands/ToolingRemoveCommand.js +19 -0
- package/build/Tooling/Commands/ToolingUpdateCommand.js +30 -0
- package/build/Tooling/Commands/groups.js +4 -0
- package/build/Tooling/Models/ToolingEntry.js +1 -0
- package/build/Tooling/Services/ToolingService.js +278 -0
- package/build/Tooling/ToolingServiceProvider.js +15 -0
- package/build/cli.js +36 -0
- package/build/index.js +31 -0
- package/package.json +54 -3
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { blueprintCatalog } from "../../BlueprintManagement/Catalog/BlueprintCatalog.js";
|
|
4
|
+
import { Service } from "../../Foundation/Service.js";
|
|
5
|
+
import { FEATURE_DIRECTORY, EXECUTION_STATES, TICKET_CATEGORIES, TRACKER_STATUSES, } from "../Models/Workflow.js";
|
|
6
|
+
import { isImplementationAssignment, isCorrectionAttempt, isCorrectionAdjudication, hasMatchingCorrectionAttempt, } from "./AssignmentSchema.js";
|
|
7
|
+
import { isCheckRequirement, isCheckRecord, isCheckGateInvalidation, } from "./CheckSchema.js";
|
|
8
|
+
import { isReviewFinding, isRequirementsDecision, } from "./FindingSchema.js";
|
|
9
|
+
import { isDeliveryProgress, isAcceptedEvidence, isIntegrationRecord, } from "./ProgressSchema.js";
|
|
10
|
+
import { isReviewAssignment, isSpecialistReviewRequirement, } from "./ReviewSchema.js";
|
|
11
|
+
import { Records } from "../../Support/Records.js";
|
|
12
|
+
const isRecord = Records.isRecord;
|
|
13
|
+
export class LedgerRepository extends Service {
|
|
14
|
+
constructor(app) {
|
|
15
|
+
super(app);
|
|
16
|
+
}
|
|
17
|
+
readLedger(feature) {
|
|
18
|
+
return this.validateLedger(JSON.parse(readFileSync(feature.ledger, "utf8")), feature.feature);
|
|
19
|
+
}
|
|
20
|
+
validateLedger(value, feature) {
|
|
21
|
+
this.assertLedger(value, feature);
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
assertLedger(value, feature) {
|
|
25
|
+
if (!isRecord(value))
|
|
26
|
+
throw new Error("Feature ledger must contain a JSON object.");
|
|
27
|
+
const id = FEATURE_DIRECTORY.exec(feature)?.[1];
|
|
28
|
+
if (value.schemaVersion !== 3) {
|
|
29
|
+
throw new Error(`Unsupported feature ledger schema version: ${String(value.schemaVersion)}.`);
|
|
30
|
+
}
|
|
31
|
+
if (typeof value.featureId !== "string" || value.featureId !== id) {
|
|
32
|
+
throw new Error("Feature ledger ID does not match its workspace directory.");
|
|
33
|
+
}
|
|
34
|
+
if (typeof value.title !== "string" || !value.title.trim()) {
|
|
35
|
+
throw new Error("Feature ledger title must be a non-empty string.");
|
|
36
|
+
}
|
|
37
|
+
if (typeof value.createdBy !== "string" || !value.createdBy.trim()) {
|
|
38
|
+
throw new Error("Feature ledger author must be a non-empty string.");
|
|
39
|
+
}
|
|
40
|
+
if (typeof value.createdAt !== "string" || !Number.isFinite(Date.parse(value.createdAt))) {
|
|
41
|
+
throw new Error("Feature ledger creation time must be a valid timestamp.");
|
|
42
|
+
}
|
|
43
|
+
if (typeof value.retrospectiveRequired !== "boolean") {
|
|
44
|
+
throw new Error("Feature ledger retrospective policy must be a boolean.");
|
|
45
|
+
}
|
|
46
|
+
if (!isRecord(value.reviewPolicy))
|
|
47
|
+
throw new Error("Feature review policy must be an object.");
|
|
48
|
+
if (typeof value.reviewPolicy.version !== "number" ||
|
|
49
|
+
!Number.isSafeInteger(value.reviewPolicy.version) ||
|
|
50
|
+
value.reviewPolicy.version < 1) {
|
|
51
|
+
throw new Error("Feature review policy version must be a positive integer.");
|
|
52
|
+
}
|
|
53
|
+
for (const role of ["ticket", "delivery"]) {
|
|
54
|
+
const reviewers = value.reviewPolicy[role];
|
|
55
|
+
if (!Array.isArray(reviewers) ||
|
|
56
|
+
reviewers.length === 0 ||
|
|
57
|
+
!reviewers.every((reviewer) => typeof reviewer === "string" && reviewer.trim())) {
|
|
58
|
+
throw new Error(`Feature ${role} review requirements must be a non-empty string list.`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!Array.isArray(value.tickets) ||
|
|
62
|
+
!value.tickets.every((ticket) => isRecord(ticket) &&
|
|
63
|
+
typeof ticket.id === "string" &&
|
|
64
|
+
/^T-\d{4,}$/.test(ticket.id) &&
|
|
65
|
+
typeof ticket.execution === "string" &&
|
|
66
|
+
EXECUTION_STATES.includes(ticket.execution) &&
|
|
67
|
+
typeof ticket.title === "string" &&
|
|
68
|
+
!!ticket.title.trim() &&
|
|
69
|
+
typeof ticket.reporter === "string" &&
|
|
70
|
+
!!ticket.reporter.trim() &&
|
|
71
|
+
typeof ticket.category === "string" &&
|
|
72
|
+
TICKET_CATEGORIES.includes(ticket.category) &&
|
|
73
|
+
typeof ticket.status === "string" &&
|
|
74
|
+
TRACKER_STATUSES.includes(ticket.status) &&
|
|
75
|
+
Array.isArray(ticket.blockedBy) &&
|
|
76
|
+
ticket.blockedBy.every((dependency) => typeof dependency === "string") &&
|
|
77
|
+
(!Object.hasOwn(ticket, "blocker") ||
|
|
78
|
+
(typeof ticket.blocker === "string" && !!ticket.blocker.trim())) &&
|
|
79
|
+
((!Object.hasOwn(ticket, "owner") && !Object.hasOwn(ticket, "baseSha")) ||
|
|
80
|
+
(typeof ticket.owner === "string" &&
|
|
81
|
+
!!ticket.owner.trim() &&
|
|
82
|
+
typeof ticket.baseSha === "string" &&
|
|
83
|
+
/^[0-9a-f]{40,64}$/i.test(ticket.baseSha))) &&
|
|
84
|
+
(!Object.hasOwn(ticket, "acceptedEvidence") ||
|
|
85
|
+
isAcceptedEvidence(ticket.acceptedEvidence)) &&
|
|
86
|
+
(!Object.hasOwn(ticket, "integration") ||
|
|
87
|
+
(ticket.execution === "integrated" &&
|
|
88
|
+
isIntegrationRecord(ticket.integration) &&
|
|
89
|
+
isAcceptedEvidence(ticket.acceptedEvidence) &&
|
|
90
|
+
JSON.stringify(ticket.integration.files) ===
|
|
91
|
+
JSON.stringify(ticket.acceptedEvidence.files) &&
|
|
92
|
+
ticket.integration.contentDigest === ticket.acceptedEvidence.contentDigest)) &&
|
|
93
|
+
(!Object.hasOwn(ticket, "reviewRequirements") ||
|
|
94
|
+
(Array.isArray(ticket.reviewRequirements) &&
|
|
95
|
+
ticket.reviewRequirements.every(isSpecialistReviewRequirement) &&
|
|
96
|
+
new Set(ticket.reviewRequirements.map((requirement) => requirement.agent)).size ===
|
|
97
|
+
ticket.reviewRequirements.length)) &&
|
|
98
|
+
(!Object.hasOwn(ticket, "checks") ||
|
|
99
|
+
(isRecord(ticket.checks) &&
|
|
100
|
+
Array.isArray(ticket.checks.required) &&
|
|
101
|
+
ticket.checks.required.every(isCheckRequirement) &&
|
|
102
|
+
Array.isArray(ticket.checks.records) &&
|
|
103
|
+
ticket.checks.records.every(isCheckRecord) &&
|
|
104
|
+
(!Object.hasOwn(ticket.checks, "invalidations") ||
|
|
105
|
+
(Array.isArray(ticket.checks.invalidations) &&
|
|
106
|
+
ticket.checks.invalidations.every(isCheckGateInvalidation))))) &&
|
|
107
|
+
(!Object.hasOwn(ticket, "assignment") ||
|
|
108
|
+
(isImplementationAssignment(ticket.assignment) &&
|
|
109
|
+
ticket.assignment.report ===
|
|
110
|
+
join(this.app.workspaceRoot, ".scratch", feature, "reports", ticket.assignment.action === "implement"
|
|
111
|
+
? `${ticket.id}-implementation.md`
|
|
112
|
+
: `${ticket.id}-round-${ticket.assignment.attempt}-fix.md`))) &&
|
|
113
|
+
(!Object.hasOwn(ticket, "correctionAttempts") ||
|
|
114
|
+
(Array.isArray(ticket.correctionAttempts) &&
|
|
115
|
+
ticket.correctionAttempts.every(isCorrectionAttempt) &&
|
|
116
|
+
ticket.correctionAttempts.every((attempt, index) => attempt.number === index + 1) &&
|
|
117
|
+
ticket.correctionAttempts.every((attempt) => attempt.assignment.report ===
|
|
118
|
+
join(this.app.workspaceRoot, ".scratch", feature, "reports", `${ticket.id}-round-${attempt.number}-fix.md`) &&
|
|
119
|
+
(!attempt.priorAssignment ||
|
|
120
|
+
attempt.priorAssignment.report ===
|
|
121
|
+
join(this.app.workspaceRoot, ".scratch", feature, "reports", attempt.priorAssignment.action === "implement"
|
|
122
|
+
? `${ticket.id}-implementation.md`
|
|
123
|
+
: `${ticket.id}-round-${attempt.priorAssignment.attempt}-fix.md`)) &&
|
|
124
|
+
(attempt.reviewAssignments ?? []).every((assignment) => assignment.report ===
|
|
125
|
+
join(this.app.workspaceRoot, ".scratch", feature, "reports", assignment.scope === "initial"
|
|
126
|
+
? `${ticket.id}-review-${assignment.id}.md`
|
|
127
|
+
: `${ticket.id}-round-${assignment.attempt}-review-${assignment.id}.md`))) &&
|
|
128
|
+
hasMatchingCorrectionAttempt(ticket))) &&
|
|
129
|
+
(!Object.hasOwn(ticket, "adjudication") ||
|
|
130
|
+
isCorrectionAdjudication(ticket.adjudication)) &&
|
|
131
|
+
(!Object.hasOwn(ticket, "reviewAssignments") ||
|
|
132
|
+
(Array.isArray(ticket.reviewAssignments) &&
|
|
133
|
+
ticket.reviewAssignments.every((assignment) => isReviewAssignment(assignment) &&
|
|
134
|
+
blueprintCatalog.agents[assignment.agent]?.canReview === true &&
|
|
135
|
+
assignment.report ===
|
|
136
|
+
join(this.app.workspaceRoot, ".scratch", feature, "reports", assignment.scope === "initial"
|
|
137
|
+
? `${ticket.id}-review-${assignment.id}.md`
|
|
138
|
+
: `${ticket.id}-round-${assignment.attempt}-review-${assignment.id}.md`)))) &&
|
|
139
|
+
(!Object.hasOwn(ticket, "reviewHistory") ||
|
|
140
|
+
(Array.isArray(ticket.reviewHistory) &&
|
|
141
|
+
ticket.reviewHistory.every((assignment) => isReviewAssignment(assignment) &&
|
|
142
|
+
blueprintCatalog.agents[assignment.agent]?.canReview === true &&
|
|
143
|
+
assignment.report ===
|
|
144
|
+
join(this.app.workspaceRoot, ".scratch", feature, "reports", assignment.scope === "initial"
|
|
145
|
+
? `${ticket.id}-review-${assignment.id}.md`
|
|
146
|
+
: `${ticket.id}-round-${assignment.attempt}-review-${assignment.id}.md`)))) &&
|
|
147
|
+
(!Object.hasOwn(ticket, "requirementsDecisions") ||
|
|
148
|
+
(Array.isArray(ticket.requirementsDecisions) &&
|
|
149
|
+
ticket.requirementsDecisions.every(isRequirementsDecision))) &&
|
|
150
|
+
(!Object.hasOwn(ticket, "findings") ||
|
|
151
|
+
(Array.isArray(ticket.findings) && ticket.findings.every(isReviewFinding))))) {
|
|
152
|
+
throw new Error("Feature ticket records must contain a ticket ID and a valid execution state.");
|
|
153
|
+
}
|
|
154
|
+
if (new Set(value.tickets.map((ticket) => ticket.id)).size !==
|
|
155
|
+
value.tickets.length) {
|
|
156
|
+
throw new Error("Feature ledger ticket IDs must be unique.");
|
|
157
|
+
}
|
|
158
|
+
for (const ticket of value.tickets) {
|
|
159
|
+
for (const requirement of ticket.reviewRequirements ?? []) {
|
|
160
|
+
if (blueprintCatalog.agents[requirement.agent]?.canReview !== true) {
|
|
161
|
+
throw new Error(`Required specialist agent '${requirement.agent}' is not explicitly marked review-eligible.`);
|
|
162
|
+
}
|
|
163
|
+
if (value.reviewPolicy.ticket.includes(requirement.agent)) {
|
|
164
|
+
throw new Error(`Specialist reviewer '${requirement.agent}' duplicates the pinned ticket review policy.`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const findingIds = ticket.findings?.map((finding) => finding.id) ?? [];
|
|
168
|
+
if (new Set(findingIds).size !== findingIds.length) {
|
|
169
|
+
throw new Error(`Finding IDs must be unique within ticket ${ticket.id}.`);
|
|
170
|
+
}
|
|
171
|
+
const decisionIds = ticket.requirementsDecisions?.map((decision) => decision.id) ?? [];
|
|
172
|
+
if (new Set(decisionIds).size !== decisionIds.length) {
|
|
173
|
+
throw new Error(`Requirements decision IDs must be unique within ticket ${ticket.id}.`);
|
|
174
|
+
}
|
|
175
|
+
for (const finding of ticket.findings ?? []) {
|
|
176
|
+
if (finding.requirementsDecision &&
|
|
177
|
+
!ticket.requirementsDecisions?.some((decision) => decision.id === finding.requirementsDecision &&
|
|
178
|
+
decision.finding === finding.id &&
|
|
179
|
+
decision.status === "approved")) {
|
|
180
|
+
throw new Error(`Finding ${finding.id} references an invalid requirements decision.`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
for (const assignment of [
|
|
184
|
+
...(ticket.reviewHistory ?? []),
|
|
185
|
+
...(ticket.reviewAssignments ?? []),
|
|
186
|
+
]) {
|
|
187
|
+
const reported = (ticket.findings ?? []).filter((finding) => finding.provenance.some((source) => source.report === assignment.report && source.snapshot === assignment.snapshot));
|
|
188
|
+
if (assignment.noFindings === true && reported.length > 0) {
|
|
189
|
+
throw new Error(`Review assignment ${assignment.id} declares no findings despite registered provenance.`);
|
|
190
|
+
}
|
|
191
|
+
for (const id of assignment.findingIds ?? []) {
|
|
192
|
+
const finding = (ticket.findings ?? []).find((candidate) => candidate.id === id);
|
|
193
|
+
if (!finding ||
|
|
194
|
+
!finding.provenance.some((source) => source.report === assignment.report && source.snapshot === assignment.snapshot)) {
|
|
195
|
+
throw new Error(`Review assignment ${assignment.id} references finding ${id} without matching provenance.`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (assignment.status === "submitted" &&
|
|
199
|
+
assignment.findingIds &&
|
|
200
|
+
(reported.length !== assignment.findingIds.length ||
|
|
201
|
+
reported.some((finding) => !assignment.findingIds.includes(finding.id)))) {
|
|
202
|
+
throw new Error(`Review assignment ${assignment.id} does not list the complete canonical findings set.`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const reviewAssignmentIds = value.tickets.flatMap((ticket) => isRecord(ticket)
|
|
207
|
+
? [
|
|
208
|
+
...(Array.isArray(ticket.reviewHistory) ? ticket.reviewHistory : []),
|
|
209
|
+
...(Array.isArray(ticket.reviewAssignments) ? ticket.reviewAssignments : []),
|
|
210
|
+
].map((assignment) => (isRecord(assignment) ? assignment.id : undefined))
|
|
211
|
+
: []);
|
|
212
|
+
if (reviewAssignmentIds.some((assignmentId) => typeof assignmentId !== "string") ||
|
|
213
|
+
new Set(reviewAssignmentIds).size !== reviewAssignmentIds.length) {
|
|
214
|
+
throw new Error("Feature review assignment IDs must be unique.");
|
|
215
|
+
}
|
|
216
|
+
if (!isRecord(value.delivery))
|
|
217
|
+
throw new Error("Feature delivery progress must be an object.");
|
|
218
|
+
if (typeof value.delivery.execution !== "string" ||
|
|
219
|
+
!EXECUTION_STATES.includes(value.delivery.execution)) {
|
|
220
|
+
throw new Error("Feature delivery execution state is invalid.");
|
|
221
|
+
}
|
|
222
|
+
if (!["active", "complete"].includes(String(value.delivery.status))) {
|
|
223
|
+
throw new Error("Feature tracking status is invalid.");
|
|
224
|
+
}
|
|
225
|
+
if (!Array.isArray(value.delivery.history) ||
|
|
226
|
+
!value.delivery.history.every((entry) => isRecord(entry) &&
|
|
227
|
+
typeof entry.at === "string" &&
|
|
228
|
+
Number.isFinite(Date.parse(entry.at)) &&
|
|
229
|
+
typeof entry.author === "string" &&
|
|
230
|
+
entry.author.trim() &&
|
|
231
|
+
typeof entry.action === "string" &&
|
|
232
|
+
entry.action.trim())) {
|
|
233
|
+
throw new Error("Feature delivery history records must contain a valid timestamp, author, and action.");
|
|
234
|
+
}
|
|
235
|
+
const delivery = value.delivery;
|
|
236
|
+
if ((Object.hasOwn(delivery, "baseSha") &&
|
|
237
|
+
(typeof delivery.baseSha !== "string" || !/^[0-9a-f]{40,64}$/i.test(delivery.baseSha))) ||
|
|
238
|
+
(Object.hasOwn(delivery, "owner") &&
|
|
239
|
+
(typeof delivery.owner !== "string" || !delivery.owner.trim())) ||
|
|
240
|
+
(Object.hasOwn(delivery, "reviewRequirements") &&
|
|
241
|
+
(!Array.isArray(delivery.reviewRequirements) ||
|
|
242
|
+
!delivery.reviewRequirements.every(isSpecialistReviewRequirement) ||
|
|
243
|
+
new Set(delivery.reviewRequirements.map((item) => item.agent)).size !==
|
|
244
|
+
delivery.reviewRequirements.length)) ||
|
|
245
|
+
(Object.hasOwn(delivery, "reviewAssignments") &&
|
|
246
|
+
(!Array.isArray(delivery.reviewAssignments) ||
|
|
247
|
+
!delivery.reviewAssignments.every((assignment) => isReviewAssignment(assignment) &&
|
|
248
|
+
assignment.scope === "delivery" &&
|
|
249
|
+
blueprintCatalog.agents[assignment.agent]?.canReview === true &&
|
|
250
|
+
assignment.report ===
|
|
251
|
+
join(this.app.workspaceRoot, ".scratch", feature, "reports", assignment.attempt
|
|
252
|
+
? `final-round-${assignment.attempt}-review-${assignment.id}.md`
|
|
253
|
+
: `final-review-${assignment.id}.md`)))) ||
|
|
254
|
+
(Object.hasOwn(delivery, "reviewHistory") &&
|
|
255
|
+
(!Array.isArray(delivery.reviewHistory) ||
|
|
256
|
+
!delivery.reviewHistory.every((assignment) => isReviewAssignment(assignment) &&
|
|
257
|
+
assignment.scope === "delivery" &&
|
|
258
|
+
blueprintCatalog.agents[assignment.agent]?.canReview === true))) ||
|
|
259
|
+
(Object.hasOwn(delivery, "requirementsDecisions") &&
|
|
260
|
+
(!Array.isArray(delivery.requirementsDecisions) ||
|
|
261
|
+
!delivery.requirementsDecisions.every(isRequirementsDecision) ||
|
|
262
|
+
new Set(delivery.requirementsDecisions.map((decision) => decision.id)).size !==
|
|
263
|
+
delivery.requirementsDecisions.length)) ||
|
|
264
|
+
(Object.hasOwn(delivery, "findings") &&
|
|
265
|
+
(!Array.isArray(delivery.findings) || !delivery.findings.every(isReviewFinding))) ||
|
|
266
|
+
(Object.hasOwn(delivery, "assignment") &&
|
|
267
|
+
(!isImplementationAssignment(delivery.assignment) ||
|
|
268
|
+
delivery.assignment.action !== "correct" ||
|
|
269
|
+
!Number.isSafeInteger(delivery.assignment.attempt) ||
|
|
270
|
+
delivery.assignment.report !==
|
|
271
|
+
join(this.app.workspaceRoot, ".scratch", feature, "reports", `final-round-${delivery.assignment.attempt}-fix.md`))) ||
|
|
272
|
+
(Object.hasOwn(delivery, "checks") &&
|
|
273
|
+
(!isRecord(delivery.checks) ||
|
|
274
|
+
!Array.isArray(delivery.checks.required) ||
|
|
275
|
+
!delivery.checks.required.every(isCheckRequirement) ||
|
|
276
|
+
!Array.isArray(delivery.checks.records) ||
|
|
277
|
+
!delivery.checks.records.every(isCheckRecord))) ||
|
|
278
|
+
(Object.hasOwn(delivery, "correctionAttempts") &&
|
|
279
|
+
(!Array.isArray(delivery.correctionAttempts) ||
|
|
280
|
+
!delivery.correctionAttempts.every(isCorrectionAttempt) ||
|
|
281
|
+
delivery.correctionAttempts.some((attempt, index) => attempt.number !== index + 1 ||
|
|
282
|
+
attempt.assignment.action !== "correct" ||
|
|
283
|
+
attempt.assignment.report !==
|
|
284
|
+
join(this.app.workspaceRoot, ".scratch", feature, "reports", `final-round-${attempt.number}-fix.md`) ||
|
|
285
|
+
(attempt.reviewAssignments ?? []).some((assignment) => assignment.scope !== "delivery" ||
|
|
286
|
+
assignment.attempt !== attempt.number ||
|
|
287
|
+
assignment.report !==
|
|
288
|
+
join(this.app.workspaceRoot, ".scratch", feature, "reports", `final-round-${attempt.number}-review-${assignment.id}.md`))) ||
|
|
289
|
+
!hasMatchingCorrectionAttempt(delivery))) ||
|
|
290
|
+
(Object.hasOwn(delivery, "adjudication") && !isCorrectionAdjudication(delivery.adjudication))) {
|
|
291
|
+
throw new Error("Feature delivery evidence or assignment records are invalid.");
|
|
292
|
+
}
|
|
293
|
+
if (!isDeliveryProgress(delivery)) {
|
|
294
|
+
throw new Error("Feature delivery progress does not match the delivery ledger schema.");
|
|
295
|
+
}
|
|
296
|
+
const deliveryFindingIds = delivery.findings?.map((finding) => finding.id) ?? [];
|
|
297
|
+
if (new Set(deliveryFindingIds).size !== deliveryFindingIds.length) {
|
|
298
|
+
throw new Error("Delivery finding IDs must be unique.");
|
|
299
|
+
}
|
|
300
|
+
const ticketIds = new Set(value.tickets.map((ticket) => ticket.id));
|
|
301
|
+
for (const finding of delivery.findings ?? []) {
|
|
302
|
+
if (finding.relatedTicket && !ticketIds.has(finding.relatedTicket)) {
|
|
303
|
+
throw new Error(`Delivery finding ${finding.id} references missing ticket ${finding.relatedTicket}.`);
|
|
304
|
+
}
|
|
305
|
+
if (finding.requirementsDecision &&
|
|
306
|
+
!delivery.requirementsDecisions?.some((decision) => decision.id === finding.requirementsDecision &&
|
|
307
|
+
decision.finding === finding.id &&
|
|
308
|
+
decision.status === "approved")) {
|
|
309
|
+
throw new Error(`Delivery finding ${finding.id} references an invalid requirements decision.`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const allReviewAssignmentIds = [
|
|
313
|
+
...reviewAssignmentIds,
|
|
314
|
+
...(Array.isArray(delivery.reviewHistory) ? delivery.reviewHistory : []).map((assignment) => isRecord(assignment) ? assignment.id : undefined),
|
|
315
|
+
...(Array.isArray(delivery.reviewAssignments) ? delivery.reviewAssignments : []).map((assignment) => (isRecord(assignment) ? assignment.id : undefined)),
|
|
316
|
+
];
|
|
317
|
+
if (allReviewAssignmentIds.some((assignmentId) => typeof assignmentId !== "string") ||
|
|
318
|
+
new Set(allReviewAssignmentIds).size !== allReviewAssignmentIds.length) {
|
|
319
|
+
throw new Error("Feature review assignment IDs must be unique.");
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
import { Service } from "../../Foundation/Service.js";
|
|
4
|
+
import { lstatIfPresent, isPathInside } from "./FileSystem.js";
|
|
5
|
+
export class PathSafety extends Service {
|
|
6
|
+
git;
|
|
7
|
+
constructor(app, git) {
|
|
8
|
+
super(app);
|
|
9
|
+
this.git = git;
|
|
10
|
+
}
|
|
11
|
+
assertTicketPath(workspace, path) {
|
|
12
|
+
const ticketsDirectory = join(workspace, "tickets");
|
|
13
|
+
const directoryEntry = lstatIfPresent(ticketsDirectory);
|
|
14
|
+
if (!directoryEntry?.isDirectory() || directoryEntry.isSymbolicLink()) {
|
|
15
|
+
throw new Error("Feature tickets path must be a real directory.");
|
|
16
|
+
}
|
|
17
|
+
if (!isPathInside(ticketsDirectory, path) ||
|
|
18
|
+
!/^T-\d{4,}\.md$/.test(path.slice(ticketsDirectory.length + 1))) {
|
|
19
|
+
throw new Error("Ticket path must be a numbered Markdown file inside tickets/.");
|
|
20
|
+
}
|
|
21
|
+
const entry = lstatIfPresent(path);
|
|
22
|
+
if (entry && !entry.isFile())
|
|
23
|
+
throw new Error(`Ticket path must be a regular file: ${path}.`);
|
|
24
|
+
if (entry && !isPathInside(ticketsDirectory, realpathSync(path))) {
|
|
25
|
+
throw new Error("Ticket file must remain inside the feature tickets directory.");
|
|
26
|
+
}
|
|
27
|
+
const root = realpathSync(this.app.workspaceRoot);
|
|
28
|
+
const relativePath = relative(root, path);
|
|
29
|
+
if (this.isIgnored(root, relativePath))
|
|
30
|
+
throw new Error(`Ticket path is ignored: ${relativePath}`);
|
|
31
|
+
}
|
|
32
|
+
isIgnored(root, relativePath) {
|
|
33
|
+
const result = this.git.run([
|
|
34
|
+
"--no-pager",
|
|
35
|
+
"-c",
|
|
36
|
+
"color.ui=false",
|
|
37
|
+
"check-ignore",
|
|
38
|
+
"--quiet",
|
|
39
|
+
"--no-index",
|
|
40
|
+
"--",
|
|
41
|
+
relativePath,
|
|
42
|
+
], {
|
|
43
|
+
cwd: root,
|
|
44
|
+
encoding: "utf8",
|
|
45
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
|
|
46
|
+
});
|
|
47
|
+
if (result.error)
|
|
48
|
+
throw result.error;
|
|
49
|
+
if (result.status !== 0 && result.status !== 1) {
|
|
50
|
+
throw new Error(`git check-ignore failed: ${result.stderr.trim()}`);
|
|
51
|
+
}
|
|
52
|
+
return result.status === 0;
|
|
53
|
+
}
|
|
54
|
+
assertReadyReadable(workspace) {
|
|
55
|
+
if (lstatIfPresent(join(workspace, ".sdd-write-recovery.json"))) {
|
|
56
|
+
throw new Error("Pending ticket recovery journal; run a mutating ticket command to recover.");
|
|
57
|
+
}
|
|
58
|
+
if (lstatIfPresent(join(workspace, ".sdd-tickets.lock"))) {
|
|
59
|
+
throw new Error("Ticket updates are in progress; retry ready after the lock is released.");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { isAbsolute } from "node:path";
|
|
2
|
+
import { EXECUTION_STATES } from "../Models/Workflow.js";
|
|
3
|
+
import { isImplementationAssignment, isCorrectionAttempt, isCorrectionAdjudication, } from "./AssignmentSchema.js";
|
|
4
|
+
import { isCheckRequirement, isCheckRecord, isCheckGateInvalidation, isSafeCoveragePath, } from "./CheckSchema.js";
|
|
5
|
+
import { isReviewFinding, isRequirementsDecision, } from "./FindingSchema.js";
|
|
6
|
+
import { isReviewAssignment, isSpecialistReviewRequirement, } from "./ReviewSchema.js";
|
|
7
|
+
import { Records } from "../../Support/Records.js";
|
|
8
|
+
const isRecord = Records.isRecord;
|
|
9
|
+
export function isDeliveryProgress(value) {
|
|
10
|
+
return (isRecord(value) &&
|
|
11
|
+
(!Object.hasOwn(value, "status") || ["active", "complete"].includes(String(value.status))) &&
|
|
12
|
+
typeof value.execution === "string" &&
|
|
13
|
+
EXECUTION_STATES.some((state) => state === value.execution) &&
|
|
14
|
+
Array.isArray(value.history) &&
|
|
15
|
+
value.history.every((entry) => isRecord(entry) &&
|
|
16
|
+
typeof entry.at === "string" &&
|
|
17
|
+
Number.isFinite(Date.parse(entry.at)) &&
|
|
18
|
+
typeof entry.author === "string" &&
|
|
19
|
+
!!entry.author.trim() &&
|
|
20
|
+
typeof entry.action === "string" &&
|
|
21
|
+
!!entry.action.trim()) &&
|
|
22
|
+
(!Object.hasOwn(value, "baseSha") ||
|
|
23
|
+
(typeof value.baseSha === "string" && /^[0-9a-f]{40,64}$/i.test(value.baseSha))) &&
|
|
24
|
+
(!Object.hasOwn(value, "owner") || (typeof value.owner === "string" && !!value.owner.trim())) &&
|
|
25
|
+
(!Object.hasOwn(value, "reviewRequirements") ||
|
|
26
|
+
(Array.isArray(value.reviewRequirements) &&
|
|
27
|
+
value.reviewRequirements.every(isSpecialistReviewRequirement))) &&
|
|
28
|
+
(!Object.hasOwn(value, "reviewAssignments") ||
|
|
29
|
+
(Array.isArray(value.reviewAssignments) &&
|
|
30
|
+
value.reviewAssignments.every(isReviewAssignment))) &&
|
|
31
|
+
(!Object.hasOwn(value, "reviewHistory") ||
|
|
32
|
+
(Array.isArray(value.reviewHistory) && value.reviewHistory.every(isReviewAssignment))) &&
|
|
33
|
+
(!Object.hasOwn(value, "assignment") ||
|
|
34
|
+
(isImplementationAssignment(value.assignment) && value.assignment.action === "correct")) &&
|
|
35
|
+
(!Object.hasOwn(value, "requirementsDecisions") ||
|
|
36
|
+
(Array.isArray(value.requirementsDecisions) &&
|
|
37
|
+
value.requirementsDecisions.every(isRequirementsDecision))) &&
|
|
38
|
+
(!Object.hasOwn(value, "checks") ||
|
|
39
|
+
(isRecord(value.checks) &&
|
|
40
|
+
Array.isArray(value.checks.required) &&
|
|
41
|
+
value.checks.required.every(isCheckRequirement) &&
|
|
42
|
+
Array.isArray(value.checks.records) &&
|
|
43
|
+
value.checks.records.every(isCheckRecord) &&
|
|
44
|
+
(!Object.hasOwn(value.checks, "invalidations") ||
|
|
45
|
+
(Array.isArray(value.checks.invalidations) &&
|
|
46
|
+
value.checks.invalidations.every(isCheckGateInvalidation))))) &&
|
|
47
|
+
(!Object.hasOwn(value, "findings") ||
|
|
48
|
+
(Array.isArray(value.findings) && value.findings.every(isReviewFinding))) &&
|
|
49
|
+
(!Object.hasOwn(value, "correctionAttempts") ||
|
|
50
|
+
(Array.isArray(value.correctionAttempts) &&
|
|
51
|
+
value.correctionAttempts.every(isCorrectionAttempt))) &&
|
|
52
|
+
(!Object.hasOwn(value, "adjudication") || isCorrectionAdjudication(value.adjudication)) &&
|
|
53
|
+
(!Object.hasOwn(value, "acceptedEvidence") || isAcceptedEvidence(value.acceptedEvidence)) &&
|
|
54
|
+
(!Object.hasOwn(value, "finished") ||
|
|
55
|
+
(isRecord(value.finished) &&
|
|
56
|
+
typeof value.finished.at === "string" &&
|
|
57
|
+
Number.isFinite(Date.parse(value.finished.at)) &&
|
|
58
|
+
typeof value.finished.author === "string" &&
|
|
59
|
+
!!value.finished.author.trim() &&
|
|
60
|
+
(!Object.hasOwn(value.finished, "report") ||
|
|
61
|
+
(typeof value.finished.report === "string" && isAbsolute(value.finished.report))) &&
|
|
62
|
+
(!Object.hasOwn(value.finished, "reportSha256") ||
|
|
63
|
+
(typeof value.finished.reportSha256 === "string" &&
|
|
64
|
+
/^[0-9a-f]{64}$/i.test(value.finished.reportSha256))))));
|
|
65
|
+
}
|
|
66
|
+
export function isAcceptedEvidence(value) {
|
|
67
|
+
return (isRecord(value) &&
|
|
68
|
+
typeof value.snapshot === "string" &&
|
|
69
|
+
/^[0-9a-f]{64}$/i.test(value.snapshot) &&
|
|
70
|
+
typeof value.codeSnapshot === "string" &&
|
|
71
|
+
/^[0-9a-f]{64}$/i.test(value.codeSnapshot) &&
|
|
72
|
+
typeof value.requirementsSnapshot === "string" &&
|
|
73
|
+
/^[0-9a-f]{64}$/i.test(value.requirementsSnapshot) &&
|
|
74
|
+
Array.isArray(value.files) &&
|
|
75
|
+
value.files.length > 0 &&
|
|
76
|
+
value.files.every((path) => typeof path === "string" && isSafeCoveragePath(path)) &&
|
|
77
|
+
new Set(value.files).size === value.files.length &&
|
|
78
|
+
typeof value.contentDigest === "string" &&
|
|
79
|
+
/^[0-9a-f]{64}$/i.test(value.contentDigest));
|
|
80
|
+
}
|
|
81
|
+
export function isIntegrationRecord(value) {
|
|
82
|
+
return (isRecord(value) &&
|
|
83
|
+
typeof value.commitSha === "string" &&
|
|
84
|
+
/^[0-9a-f]{40,64}$/i.test(value.commitSha) &&
|
|
85
|
+
typeof value.integratedAt === "string" &&
|
|
86
|
+
Number.isFinite(Date.parse(value.integratedAt)) &&
|
|
87
|
+
typeof value.integratedBy === "string" &&
|
|
88
|
+
!!value.integratedBy.trim() &&
|
|
89
|
+
Array.isArray(value.files) &&
|
|
90
|
+
value.files.length > 0 &&
|
|
91
|
+
value.files.every((path) => typeof path === "string" && isSafeCoveragePath(path)) &&
|
|
92
|
+
new Set(value.files).size === value.files.length &&
|
|
93
|
+
typeof value.contentDigest === "string" &&
|
|
94
|
+
/^[0-9a-f]{64}$/i.test(value.contentDigest));
|
|
95
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { Service } from "../../Foundation/Service.js";
|
|
3
|
+
export class ReadinessReader extends Service {
|
|
4
|
+
ticketRepository;
|
|
5
|
+
ledgerRepository;
|
|
6
|
+
pathSafety;
|
|
7
|
+
constructor(app, ticketRepository, ledgerRepository, pathSafety) {
|
|
8
|
+
super(app);
|
|
9
|
+
this.ticketRepository = ticketRepository;
|
|
10
|
+
this.ledgerRepository = ledgerRepository;
|
|
11
|
+
this.pathSafety = pathSafety;
|
|
12
|
+
}
|
|
13
|
+
read(feature) {
|
|
14
|
+
let ledger;
|
|
15
|
+
let tickets;
|
|
16
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
17
|
+
this.pathSafety.assertReadyReadable(feature.workspace);
|
|
18
|
+
const ledgerRaw = readFileSync(feature.ledger, "utf8");
|
|
19
|
+
this.ledgerRepository.validateLedger(JSON.parse(ledgerRaw), feature.feature);
|
|
20
|
+
const observedTickets = this.ticketRepository.readTickets(feature.workspace);
|
|
21
|
+
this.pathSafety.assertReadyReadable(feature.workspace);
|
|
22
|
+
const confirmedLedgerRaw = readFileSync(feature.ledger, "utf8");
|
|
23
|
+
const confirmedLedger = this.ledgerRepository.validateLedger(JSON.parse(confirmedLedgerRaw), feature.feature);
|
|
24
|
+
const confirmedTickets = this.ticketRepository.readTickets(feature.workspace);
|
|
25
|
+
this.pathSafety.assertReadyReadable(feature.workspace);
|
|
26
|
+
const observedTicketContents = new Map(observedTickets.map((ticket) => [ticket.id, ticket.raw]));
|
|
27
|
+
const ticketsMatch = observedTicketContents.size === confirmedTickets.length &&
|
|
28
|
+
confirmedTickets.every((ticket) => observedTicketContents.get(ticket.id) === ticket.raw);
|
|
29
|
+
if (ledgerRaw === confirmedLedgerRaw && ticketsMatch) {
|
|
30
|
+
ledger = confirmedLedger;
|
|
31
|
+
tickets = confirmedTickets;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (!ledger || !tickets) {
|
|
36
|
+
throw new Error("Ticket data changed while reading the ready queue; retry ready.");
|
|
37
|
+
}
|
|
38
|
+
return { ledger, tickets };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { isAbsolute } from "node:path";
|
|
2
|
+
import { Records } from "../../Support/Records.js";
|
|
3
|
+
const isRecord = Records.isRecord;
|
|
4
|
+
export function isReviewAssignment(value) {
|
|
5
|
+
if (!isRecord(value) ||
|
|
6
|
+
typeof value.id !== "string" ||
|
|
7
|
+
!value.id.trim() ||
|
|
8
|
+
typeof value.agent !== "string" ||
|
|
9
|
+
!value.agent.trim() ||
|
|
10
|
+
value.role !== "review" ||
|
|
11
|
+
!["initial", "corrective", "delivery"].includes(String(value.scope)) ||
|
|
12
|
+
(value.scope === "corrective" &&
|
|
13
|
+
(!Number.isSafeInteger(value.attempt) || Number(value.attempt) < 1)) ||
|
|
14
|
+
(value.scope === "initial" && Object.hasOwn(value, "attempt")) ||
|
|
15
|
+
(value.scope === "delivery" &&
|
|
16
|
+
Object.hasOwn(value, "attempt") &&
|
|
17
|
+
(!Number.isSafeInteger(value.attempt) || Number(value.attempt) < 1)) ||
|
|
18
|
+
typeof value.snapshot !== "string" ||
|
|
19
|
+
!/^[0-9a-f]{64}$/i.test(value.snapshot) ||
|
|
20
|
+
typeof value.codeSnapshot !== "string" ||
|
|
21
|
+
!/^[0-9a-f]{64}$/i.test(value.codeSnapshot) ||
|
|
22
|
+
typeof value.requirementsSnapshot !== "string" ||
|
|
23
|
+
!/^[0-9a-f]{64}$/i.test(value.requirementsSnapshot) ||
|
|
24
|
+
typeof value.headSha !== "string" ||
|
|
25
|
+
!/^[0-9a-f]{40,64}$/i.test(value.headSha) ||
|
|
26
|
+
!Array.isArray(value.inventory) ||
|
|
27
|
+
!value.inventory.every((path) => typeof path === "string" && path.length > 0) ||
|
|
28
|
+
new Set(value.inventory).size !== value.inventory.length ||
|
|
29
|
+
(Object.hasOwn(value, "objective") &&
|
|
30
|
+
(typeof value.objective !== "string" || !value.objective.trim())) ||
|
|
31
|
+
(Object.hasOwn(value, "details") &&
|
|
32
|
+
(typeof value.details !== "string" || !value.details.trim())) ||
|
|
33
|
+
typeof value.report !== "string" ||
|
|
34
|
+
!isAbsolute(value.report) ||
|
|
35
|
+
typeof value.requestedAt !== "string" ||
|
|
36
|
+
!Number.isFinite(Date.parse(value.requestedAt)) ||
|
|
37
|
+
!["pending", "submitted"].includes(String(value.status))) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
if (value.status === "pending")
|
|
41
|
+
return true;
|
|
42
|
+
return (["passed", "failed", "unavailable"].includes(String(value.result)) &&
|
|
43
|
+
(value.noFindings === true
|
|
44
|
+
? value.findingIds === undefined
|
|
45
|
+
: value.noFindings === undefined &&
|
|
46
|
+
Array.isArray(value.findingIds) &&
|
|
47
|
+
value.findingIds.length > 0 &&
|
|
48
|
+
value.findingIds.every((id) => typeof id === "string" && /^F-\d{4,}$/.test(id)) &&
|
|
49
|
+
new Set(value.findingIds).size === value.findingIds.length) &&
|
|
50
|
+
typeof value.reportSha256 === "string" &&
|
|
51
|
+
/^[0-9a-f]{64}$/i.test(value.reportSha256) &&
|
|
52
|
+
typeof value.submittedAt === "string" &&
|
|
53
|
+
Number.isFinite(Date.parse(value.submittedAt)) &&
|
|
54
|
+
typeof value.submittedBy === "string" &&
|
|
55
|
+
!!value.submittedBy.trim() &&
|
|
56
|
+
(value.result === "passed" || (typeof value.details === "string" && !!value.details.trim())));
|
|
57
|
+
}
|
|
58
|
+
export function isSpecialistReviewRequirement(value) {
|
|
59
|
+
return (isRecord(value) &&
|
|
60
|
+
typeof value.agent === "string" &&
|
|
61
|
+
!!value.agent.trim() &&
|
|
62
|
+
typeof value.details === "string" &&
|
|
63
|
+
!!value.details.trim() &&
|
|
64
|
+
typeof value.author === "string" &&
|
|
65
|
+
!!value.author.trim() &&
|
|
66
|
+
typeof value.requestedAt === "string" &&
|
|
67
|
+
Number.isFinite(Date.parse(value.requestedAt)));
|
|
68
|
+
}
|