@use-the-fork/spindle 0.0.0-stage → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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,147 @@
|
|
|
1
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import matter from "gray-matter";
|
|
4
|
+
import { Service } from "../../Foundation/Service.js";
|
|
5
|
+
import { validateAuthor, validateTicketId, validateCategory, } from "../Policies/InputValidation.js";
|
|
6
|
+
import { Records } from "../../Support/Records.js";
|
|
7
|
+
const isRecord = Records.isRecord;
|
|
8
|
+
export class TicketRepository extends Service {
|
|
9
|
+
pathSafety;
|
|
10
|
+
ledgerRepository;
|
|
11
|
+
constructor(app, pathSafety, ledgerRepository) {
|
|
12
|
+
super(app);
|
|
13
|
+
this.pathSafety = pathSafety;
|
|
14
|
+
this.ledgerRepository = ledgerRepository;
|
|
15
|
+
}
|
|
16
|
+
readTickets(workspace) {
|
|
17
|
+
const feature = basename(workspace);
|
|
18
|
+
const ledger = this.ledgerRepository.readLedger({
|
|
19
|
+
feature,
|
|
20
|
+
workspace,
|
|
21
|
+
spec: join(workspace, "spec.md"),
|
|
22
|
+
ledger: join(workspace, "sdd.json"),
|
|
23
|
+
});
|
|
24
|
+
const progressById = new Map(ledger.tickets.map((ticket) => [ticket.id, ticket]));
|
|
25
|
+
const directory = join(workspace, "tickets");
|
|
26
|
+
const tickets = [];
|
|
27
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
28
|
+
if (!entry.name.endsWith(".md"))
|
|
29
|
+
continue;
|
|
30
|
+
const id = entry.name.slice(0, -3);
|
|
31
|
+
validateTicketId(id);
|
|
32
|
+
const path = join(directory, entry.name);
|
|
33
|
+
this.pathSafety.assertTicketPath(workspace, path);
|
|
34
|
+
const raw = readFileSync(path, "utf8");
|
|
35
|
+
const parsed = matter(raw);
|
|
36
|
+
const data = parsed.data;
|
|
37
|
+
if (!isRecord(data) || data.id !== id)
|
|
38
|
+
throw new Error(`Ticket ${id} has invalid YAML front matter.`);
|
|
39
|
+
if (["title", "reporter", "category", "status", "blocked_by", "blocker"].some((key) => Object.hasOwn(data, key))) {
|
|
40
|
+
throw new Error(`Ticket ${id} tracking metadata must be stored in sdd.json.`);
|
|
41
|
+
}
|
|
42
|
+
const progress = progressById.get(id);
|
|
43
|
+
if (!progress)
|
|
44
|
+
throw new Error(`Ticket ${id} has no ledger record.`);
|
|
45
|
+
const { title, reporter, category, status, blockedBy, blocker } = progress;
|
|
46
|
+
if (typeof title !== "string" || !title.trim())
|
|
47
|
+
throw new Error(`Ticket ${id} title is invalid.`);
|
|
48
|
+
if (typeof reporter !== "string" || !reporter.trim())
|
|
49
|
+
throw new Error(`Ticket ${id} reporter is invalid.`);
|
|
50
|
+
validateCategory(category);
|
|
51
|
+
if (status !== "open" && status !== "in-progress" && status !== "closed") {
|
|
52
|
+
throw new Error(`Ticket ${id} status is invalid.`);
|
|
53
|
+
}
|
|
54
|
+
if (!Array.isArray(blockedBy) || !blockedBy.every((blocked) => typeof blocked === "string")) {
|
|
55
|
+
throw new Error(`Ticket ${id} dependencies must be a list of ticket IDs.`);
|
|
56
|
+
}
|
|
57
|
+
if (new Set(blockedBy).size !== blockedBy.length)
|
|
58
|
+
throw new Error(`Ticket ${id} has duplicate dependencies.`);
|
|
59
|
+
for (const blocked of blockedBy)
|
|
60
|
+
validateTicketId(blocked);
|
|
61
|
+
tickets.push({
|
|
62
|
+
metadata: { ...data },
|
|
63
|
+
id,
|
|
64
|
+
title,
|
|
65
|
+
reporter,
|
|
66
|
+
category,
|
|
67
|
+
status,
|
|
68
|
+
blocked_by: [...blockedBy],
|
|
69
|
+
...(blocker ? { blocker } : {}),
|
|
70
|
+
body: parsed.content,
|
|
71
|
+
path,
|
|
72
|
+
raw,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return tickets;
|
|
76
|
+
}
|
|
77
|
+
metadata(ticket) {
|
|
78
|
+
return {
|
|
79
|
+
...ticket.metadata,
|
|
80
|
+
id: ticket.id,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
requireTicket(tickets, id) {
|
|
84
|
+
const ticket = tickets.find((item) => item.id === id);
|
|
85
|
+
if (!ticket)
|
|
86
|
+
throw new Error(`Ticket not found: ${id}.`);
|
|
87
|
+
return ticket;
|
|
88
|
+
}
|
|
89
|
+
normalizeDependencies(values) {
|
|
90
|
+
const dependencies = values.flatMap((value) => value.split(",")).map((value) => value.trim());
|
|
91
|
+
if (dependencies.some((value) => !value))
|
|
92
|
+
throw new Error("Dependency IDs must not be empty.");
|
|
93
|
+
for (const dependency of dependencies)
|
|
94
|
+
validateTicketId(dependency);
|
|
95
|
+
if (new Set(dependencies).size !== dependencies.length) {
|
|
96
|
+
throw new Error("Dependency IDs must not contain duplicates.");
|
|
97
|
+
}
|
|
98
|
+
return dependencies;
|
|
99
|
+
}
|
|
100
|
+
validateDependencyGraph(tickets) {
|
|
101
|
+
const byId = new Map(tickets.map((item) => [item.id, item]));
|
|
102
|
+
const visiting = new Set();
|
|
103
|
+
const visited = new Set();
|
|
104
|
+
const visit = (id) => {
|
|
105
|
+
if (visiting.has(id))
|
|
106
|
+
throw new Error(`Ticket dependency cycle detected at ${id}.`);
|
|
107
|
+
if (visited.has(id))
|
|
108
|
+
return;
|
|
109
|
+
const ticket = byId.get(id);
|
|
110
|
+
if (!ticket)
|
|
111
|
+
return;
|
|
112
|
+
visiting.add(id);
|
|
113
|
+
for (const dependency of ticket.blocked_by) {
|
|
114
|
+
if (!byId.has(dependency))
|
|
115
|
+
throw new Error(`Ticket ${id} references missing dependency ${dependency}.`);
|
|
116
|
+
visit(dependency);
|
|
117
|
+
}
|
|
118
|
+
visiting.delete(id);
|
|
119
|
+
visited.add(id);
|
|
120
|
+
};
|
|
121
|
+
for (const ticket of tickets)
|
|
122
|
+
visit(ticket.id);
|
|
123
|
+
}
|
|
124
|
+
appendAudit(body, author, details) {
|
|
125
|
+
const heading = /^## Comments[ \t]*\r?$/m.exec(body);
|
|
126
|
+
if (!heading)
|
|
127
|
+
throw new Error("Ticket must contain a ## Comments section.");
|
|
128
|
+
const start = heading.index + heading[0].length;
|
|
129
|
+
const nextHeading = /^#{1,2}[ \t]+/m.exec(body.slice(start));
|
|
130
|
+
const end = nextHeading ? start + nextHeading.index : body.length;
|
|
131
|
+
const content = body.slice(0, end).replace(/\s+$/, "");
|
|
132
|
+
const laterSections = body.slice(end);
|
|
133
|
+
const entry = `**${new Date().toISOString()} - ${author.trim()}**\n\n${details}`;
|
|
134
|
+
return `${content}\n\n${entry}\n${laterSections ? `\n${laterSections}` : ""}`;
|
|
135
|
+
}
|
|
136
|
+
serializeTicket(ticket, body) {
|
|
137
|
+
return matter.stringify(body, this.metadata(ticket));
|
|
138
|
+
}
|
|
139
|
+
validateTicketCreateOptions(options) {
|
|
140
|
+
if (!options.title.trim())
|
|
141
|
+
throw new Error("Title must not be empty.");
|
|
142
|
+
if (!options.reporter.trim())
|
|
143
|
+
throw new Error("Reporter must not be empty.");
|
|
144
|
+
validateCategory(options.category);
|
|
145
|
+
validateAuthor(options.author);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { chmodSync, closeSync, openSync, readFileSync, realpathSync, renameSync, rmSync, unlinkSync, writeFileSync, writeSync, } from "node:fs";
|
|
3
|
+
import { join, relative } from "node:path";
|
|
4
|
+
import { Service } from "../../Foundation/Service.js";
|
|
5
|
+
import { lstatIfPresent, waitBriefly, LOCK_TIMEOUT_MS, } from "./FileSystem.js";
|
|
6
|
+
import { Records } from "../../Support/Records.js";
|
|
7
|
+
const isRecord = Records.isRecord;
|
|
8
|
+
export class WorkspaceTransaction extends Service {
|
|
9
|
+
pathSafety;
|
|
10
|
+
activeWorkspaces = new Set();
|
|
11
|
+
run(workspace, operation) {
|
|
12
|
+
const release = this.acquireTicketLock(workspace);
|
|
13
|
+
this.activeWorkspaces.add(workspace);
|
|
14
|
+
try {
|
|
15
|
+
this.recoverWrites(workspace);
|
|
16
|
+
return operation();
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
this.activeWorkspaces.delete(workspace);
|
|
20
|
+
release();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
constructor(app, pathSafety) {
|
|
24
|
+
super(app);
|
|
25
|
+
this.pathSafety = pathSafety;
|
|
26
|
+
}
|
|
27
|
+
acquireTicketLock(workspace) {
|
|
28
|
+
const lockPath = join(workspace, ".sdd-tickets.lock");
|
|
29
|
+
const root = realpathSync(this.app.workspaceRoot);
|
|
30
|
+
const relativeLock = relative(root, lockPath);
|
|
31
|
+
if (this.pathSafety.isIgnored(root, relativeLock)) {
|
|
32
|
+
throw new Error(`Ticket lock path is ignored: ${relativeLock}`);
|
|
33
|
+
}
|
|
34
|
+
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
35
|
+
while (true) {
|
|
36
|
+
try {
|
|
37
|
+
const descriptor = openSync(lockPath, "wx", 0o600);
|
|
38
|
+
try {
|
|
39
|
+
writeSync(descriptor, JSON.stringify({ pid: process.pid }));
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
closeSync(descriptor);
|
|
43
|
+
unlinkSync(lockPath);
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
return () => {
|
|
47
|
+
try {
|
|
48
|
+
closeSync(descriptor);
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
unlinkSync(lockPath);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (typeof error !== "object" ||
|
|
57
|
+
error === null ||
|
|
58
|
+
!("code" in error) ||
|
|
59
|
+
error.code !== "EEXIST")
|
|
60
|
+
throw error;
|
|
61
|
+
if (this.staleLockReason(lockPath)) {
|
|
62
|
+
const lockEntry = lstatIfPresent(lockPath);
|
|
63
|
+
if (!lockEntry?.isFile() || lockEntry.isSymbolicLink()) {
|
|
64
|
+
throw new Error(`Ticket lock path is not a regular file and was preserved: ${lockPath}.`);
|
|
65
|
+
}
|
|
66
|
+
unlinkSync(lockPath);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (Date.now() >= deadline) {
|
|
70
|
+
throw new Error(`Timed out waiting for ticket updates; inspect ${lockPath} before retrying.`);
|
|
71
|
+
}
|
|
72
|
+
waitBriefly();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
recoverWrites(workspace) {
|
|
77
|
+
const journalPath = join(workspace, ".sdd-write-recovery.json");
|
|
78
|
+
const entry = lstatIfPresent(journalPath);
|
|
79
|
+
if (!entry)
|
|
80
|
+
return;
|
|
81
|
+
if (!entry.isFile() || entry.isSymbolicLink())
|
|
82
|
+
throw new Error("Ticket recovery journal is not a regular file.");
|
|
83
|
+
const value = JSON.parse(readFileSync(journalPath, "utf8"));
|
|
84
|
+
if (!isRecord(value) || value.version !== 1 || !Array.isArray(value.writes)) {
|
|
85
|
+
throw new Error("Ticket recovery journal has an unsupported format.");
|
|
86
|
+
}
|
|
87
|
+
const writes = value.writes;
|
|
88
|
+
const targets = new Set();
|
|
89
|
+
const pending = [];
|
|
90
|
+
for (const write of writes) {
|
|
91
|
+
if (!isRecord(write) ||
|
|
92
|
+
typeof write.path !== "string" ||
|
|
93
|
+
!(write.before === null || typeof write.before === "string") ||
|
|
94
|
+
typeof write.after !== "string" ||
|
|
95
|
+
!(write.path === "sdd.json" || /^tickets\/T-\d{4,}\.md$/.test(write.path)))
|
|
96
|
+
throw new Error("Ticket recovery journal contains an invalid write target.");
|
|
97
|
+
if (targets.has(write.path))
|
|
98
|
+
throw new Error("Ticket recovery journal contains duplicate write targets.");
|
|
99
|
+
targets.add(write.path);
|
|
100
|
+
const target = join(workspace, write.path);
|
|
101
|
+
const root = realpathSync(this.app.workspaceRoot);
|
|
102
|
+
const relativeTarget = relative(root, target);
|
|
103
|
+
if (this.pathSafety.isIgnored(root, relativeTarget)) {
|
|
104
|
+
throw new Error(`Ticket write target is ignored: ${relativeTarget}`);
|
|
105
|
+
}
|
|
106
|
+
if (write.path !== "sdd.json")
|
|
107
|
+
this.pathSafety.assertTicketPath(workspace, target);
|
|
108
|
+
else {
|
|
109
|
+
const ledgerEntry = lstatIfPresent(target);
|
|
110
|
+
if (!ledgerEntry?.isFile() || ledgerEntry.isSymbolicLink()) {
|
|
111
|
+
throw new Error("Feature ledger must be a regular file during ticket recovery.");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const current = lstatIfPresent(target) ? readFileSync(target, "utf8") : null;
|
|
115
|
+
if (current !== write.before && current !== write.after) {
|
|
116
|
+
throw new Error(`Ticket recovery found conflicting external content at ${write.path}; preserved all files.`);
|
|
117
|
+
}
|
|
118
|
+
if (current !== write.after)
|
|
119
|
+
pending.push({ target, after: write.after });
|
|
120
|
+
}
|
|
121
|
+
for (const write of pending)
|
|
122
|
+
this.atomicWrite(write.target, write.after);
|
|
123
|
+
unlinkSync(journalPath);
|
|
124
|
+
}
|
|
125
|
+
applyWrites(workspace, writes) {
|
|
126
|
+
if (!this.activeWorkspaces.has(workspace)) {
|
|
127
|
+
throw new Error("Workflow writes require the workspace transaction lock.");
|
|
128
|
+
}
|
|
129
|
+
const journalPath = join(workspace, ".sdd-write-recovery.json");
|
|
130
|
+
if (lstatIfPresent(journalPath)) {
|
|
131
|
+
throw new Error("A pending ticket write recovery journal must be resolved first.");
|
|
132
|
+
}
|
|
133
|
+
for (const write of writes) {
|
|
134
|
+
if (!(write.path === "sdd.json" || /^tickets\/T-\d{4,}\.md$/.test(write.path))) {
|
|
135
|
+
throw new Error(`Unsafe ticket write target: ${write.path}.`);
|
|
136
|
+
}
|
|
137
|
+
const root = realpathSync(this.app.workspaceRoot);
|
|
138
|
+
const target = join(workspace, write.path);
|
|
139
|
+
const relativeTarget = relative(root, target);
|
|
140
|
+
if (this.pathSafety.isIgnored(root, relativeTarget)) {
|
|
141
|
+
throw new Error(`Ticket write target is ignored: ${relativeTarget}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const root = realpathSync(this.app.workspaceRoot);
|
|
145
|
+
const relativeJournal = relative(root, journalPath);
|
|
146
|
+
if (this.pathSafety.isIgnored(root, relativeJournal)) {
|
|
147
|
+
throw new Error(`Ticket recovery path is ignored: ${relativeJournal}`);
|
|
148
|
+
}
|
|
149
|
+
this.atomicWrite(journalPath, `${JSON.stringify({ version: 1, writes }, null, 2)}\n`);
|
|
150
|
+
this.recoverWrites(workspace);
|
|
151
|
+
}
|
|
152
|
+
atomicWrite(path, contents) {
|
|
153
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
154
|
+
const existing = lstatIfPresent(path);
|
|
155
|
+
const mode = existing ? existing.mode & 0o777 : 0o666 & ~process.umask();
|
|
156
|
+
try {
|
|
157
|
+
writeFileSync(temporary, contents, { flag: "wx", mode: mode | 0o200 });
|
|
158
|
+
if ((mode | 0o200) !== mode)
|
|
159
|
+
chmodSync(temporary, mode);
|
|
160
|
+
renameSync(temporary, path);
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
rmSync(temporary, { force: true });
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
writeWorkflowState(feature, ticket, ticketAfter, ledger) {
|
|
168
|
+
this.applyWrites(feature.workspace, [
|
|
169
|
+
{
|
|
170
|
+
path: relative(feature.workspace, ticket.path),
|
|
171
|
+
before: ticket.raw,
|
|
172
|
+
after: ticketAfter,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
path: "sdd.json",
|
|
176
|
+
before: readFileSync(feature.ledger, "utf8"),
|
|
177
|
+
after: `${JSON.stringify(ledger, null, 2)}\n`,
|
|
178
|
+
},
|
|
179
|
+
]);
|
|
180
|
+
}
|
|
181
|
+
writeLedger(feature, ledger) {
|
|
182
|
+
this.applyWrites(feature.workspace, [
|
|
183
|
+
{
|
|
184
|
+
path: "sdd.json",
|
|
185
|
+
before: readFileSync(feature.ledger, "utf8"),
|
|
186
|
+
after: `${JSON.stringify(ledger, null, 2)}\n`,
|
|
187
|
+
},
|
|
188
|
+
]);
|
|
189
|
+
}
|
|
190
|
+
acquireCreationLock(scratch) {
|
|
191
|
+
const lockPath = join(scratch, ".sdd-create.lock");
|
|
192
|
+
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
193
|
+
while (true) {
|
|
194
|
+
try {
|
|
195
|
+
const descriptor = openSync(lockPath, "wx", 0o600);
|
|
196
|
+
try {
|
|
197
|
+
writeSync(descriptor, JSON.stringify({ pid: process.pid, createdAt: Date.now() }));
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
closeSync(descriptor);
|
|
201
|
+
unlinkSync(lockPath);
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
return () => {
|
|
205
|
+
try {
|
|
206
|
+
closeSync(descriptor);
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
unlinkSync(lockPath);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
if (typeof error !== "object" ||
|
|
215
|
+
error === null ||
|
|
216
|
+
!("code" in error) ||
|
|
217
|
+
error.code !== "EEXIST") {
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
const staleLock = this.staleLockReason(lockPath);
|
|
221
|
+
if (staleLock) {
|
|
222
|
+
throw new Error(`Stale feature creation lock at ${lockPath}: ${staleLock}. Verify its owner has stopped, then remove the lock and any abandoned .sdd-create-* staging directory before retrying.`);
|
|
223
|
+
}
|
|
224
|
+
if (Date.now() >= deadline) {
|
|
225
|
+
throw new Error(`Timed out waiting for another feature workspace creation; inspect ${lockPath} and remove it only after confirming its owner stopped.`);
|
|
226
|
+
}
|
|
227
|
+
waitBriefly();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
staleLockReason(lockPath) {
|
|
232
|
+
try {
|
|
233
|
+
const entry = lstatIfPresent(lockPath);
|
|
234
|
+
if (!entry)
|
|
235
|
+
return undefined;
|
|
236
|
+
if (!entry.isFile())
|
|
237
|
+
return "the lock path is not a regular file";
|
|
238
|
+
let owner;
|
|
239
|
+
try {
|
|
240
|
+
owner = JSON.parse(readFileSync(lockPath, "utf8"));
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return Date.now() - entry.mtimeMs >= LOCK_TIMEOUT_MS
|
|
244
|
+
? "the lock contents are malformed and old"
|
|
245
|
+
: undefined;
|
|
246
|
+
}
|
|
247
|
+
if (typeof owner.pid === "number" && Number.isSafeInteger(owner.pid) && owner.pid > 0) {
|
|
248
|
+
try {
|
|
249
|
+
process.kill(owner.pid, 0);
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
if (typeof error === "object" &&
|
|
254
|
+
error !== null &&
|
|
255
|
+
"code" in error &&
|
|
256
|
+
error.code === "EPERM") {
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
if (typeof error === "object" &&
|
|
260
|
+
error !== null &&
|
|
261
|
+
"code" in error &&
|
|
262
|
+
error.code !== "ESRCH") {
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
return "the recorded owner process is no longer running";
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return Date.now() - entry.mtimeMs >= LOCK_TIMEOUT_MS
|
|
269
|
+
? "the lock has no live process owner"
|
|
270
|
+
: undefined;
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
if (typeof error === "object" &&
|
|
274
|
+
error !== null &&
|
|
275
|
+
"code" in error &&
|
|
276
|
+
error.code === "ENOENT") {
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Service } from "../../Foundation/Service.js";
|
|
2
|
+
import { requiredTicketReviewers } from "./RequiredReviewers.js";
|
|
3
|
+
export class AcceptancePolicy extends Service {
|
|
4
|
+
assignmentPolicy;
|
|
5
|
+
checkPolicy;
|
|
6
|
+
findingPolicy;
|
|
7
|
+
reviewPolicy;
|
|
8
|
+
constructor(app, assignmentPolicy, checkPolicy, findingPolicy, reviewPolicy) {
|
|
9
|
+
super(app);
|
|
10
|
+
this.assignmentPolicy = assignmentPolicy;
|
|
11
|
+
this.checkPolicy = checkPolicy;
|
|
12
|
+
this.findingPolicy = findingPolicy;
|
|
13
|
+
this.reviewPolicy = reviewPolicy;
|
|
14
|
+
}
|
|
15
|
+
acceptBlockers(feature, ledger, ticket, progress, identity) {
|
|
16
|
+
const blockers = this.checkPolicy.reviewCheckBlockers(feature, ticket, progress, identity);
|
|
17
|
+
blockers.push(...this.findingPolicy.findingBlockers(progress, identity));
|
|
18
|
+
for (const decision of progress.requirementsDecisions ?? []) {
|
|
19
|
+
if (decision.status === "rejected" &&
|
|
20
|
+
decision.requirementsSnapshot === identity.requirementsSnapshot) {
|
|
21
|
+
blockers.push(`Requirements decision ${decision.id} was rejected; acceptance requires an approved decision and fresh review.`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const implementationBlocker = this.assignmentPolicy.implementationReportBlocker(feature, progress.assignment);
|
|
25
|
+
if (implementationBlocker)
|
|
26
|
+
blockers.push(implementationBlocker);
|
|
27
|
+
const assignments = progress.reviewAssignments ?? [];
|
|
28
|
+
const requiredReviewers = requiredTicketReviewers(ledger, progress);
|
|
29
|
+
if (new Set(assignments.map((assignment) => assignment.id)).size !== assignments.length ||
|
|
30
|
+
new Set(assignments.map((assignment) => assignment.report)).size !== assignments.length) {
|
|
31
|
+
blockers.push("Initial review assignments must have distinct IDs and report destinations.");
|
|
32
|
+
}
|
|
33
|
+
const hasCorrections = (progress.correctionAttempts?.length ?? 0) > 0;
|
|
34
|
+
for (const { agent, details } of requiredReviewers) {
|
|
35
|
+
const coverage = assignments.filter((assignment) => assignment.agent === agent && assignment.scope === "initial");
|
|
36
|
+
if (coverage.length === 0) {
|
|
37
|
+
blockers.push(`Required initial review coverage for '${agent}' is missing.`);
|
|
38
|
+
}
|
|
39
|
+
else if (coverage.length > 1) {
|
|
40
|
+
blockers.push(`Required initial review coverage for '${agent}' is duplicated.`);
|
|
41
|
+
}
|
|
42
|
+
else if (coverage[0].objective !== details) {
|
|
43
|
+
blockers.push(`Review assignment for '${agent}' does not match its pinned objective.`);
|
|
44
|
+
}
|
|
45
|
+
else if (hasCorrections &&
|
|
46
|
+
(coverage[0].status !== "submitted" ||
|
|
47
|
+
coverage[0].result === "unavailable" ||
|
|
48
|
+
this.reviewPolicy.reviewReportIntegrityBlocker(feature, coverage[0]))) {
|
|
49
|
+
blockers.push(`Required initial review coverage for '${agent}' is missing or its registered report is unavailable.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (hasCorrections) {
|
|
53
|
+
const currentAttempt = progress.correctionAttempts.at(-1);
|
|
54
|
+
const correctiveAssignments = currentAttempt.reviewAssignments ?? [];
|
|
55
|
+
if (correctiveAssignments.length !== requiredReviewers.length) {
|
|
56
|
+
blockers.push("Complete corrective review coverage for the latest attempt is missing.");
|
|
57
|
+
}
|
|
58
|
+
for (const { agent, details } of requiredReviewers) {
|
|
59
|
+
const coverage = correctiveAssignments.filter((assignment) => assignment.agent === agent &&
|
|
60
|
+
assignment.scope === "corrective" &&
|
|
61
|
+
assignment.attempt === currentAttempt.number);
|
|
62
|
+
if (coverage.length !== 1) {
|
|
63
|
+
blockers.push(`Required corrective review coverage for '${agent}' is missing or duplicated.`);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (coverage[0].objective !== details) {
|
|
67
|
+
blockers.push(`Corrective review assignment for '${agent}' does not match its pinned objective.`);
|
|
68
|
+
}
|
|
69
|
+
if (coverage[0].status !== "submitted" || coverage[0].result !== "passed") {
|
|
70
|
+
blockers.push(`Corrective review assignment ${coverage[0].id} is not passing.`);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const blocker = this.reviewPolicy.reviewAssignmentBlocker(feature, progress, coverage[0], identity);
|
|
74
|
+
if (blocker)
|
|
75
|
+
blockers.push(blocker);
|
|
76
|
+
}
|
|
77
|
+
for (const assignment of correctiveAssignments) {
|
|
78
|
+
if (!requiredReviewers.some((reviewer) => reviewer.agent === assignment.agent)) {
|
|
79
|
+
blockers.push(`Corrective review assignment ${assignment.id} is not part of the pinned review policy.`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
for (const assignment of assignments) {
|
|
85
|
+
if (!requiredReviewers.some((reviewer) => reviewer.agent === assignment.agent)) {
|
|
86
|
+
blockers.push(`Review assignment ${assignment.id} is not part of the pinned review policy.`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (assignment.status === "pending") {
|
|
90
|
+
blockers.push(`Review assignment ${assignment.id} is pending.`);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const blocker = this.reviewPolicy.reviewAssignmentBlocker(feature, progress, assignment, identity);
|
|
94
|
+
if (blocker)
|
|
95
|
+
blockers.push(blocker);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return [...new Set(blockers)];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Service } from "../../Foundation/Service.js";
|
|
2
|
+
export class AssignmentPolicy extends Service {
|
|
3
|
+
reportEvidence;
|
|
4
|
+
constructor(app, reportEvidence) {
|
|
5
|
+
super(app);
|
|
6
|
+
this.reportEvidence = reportEvidence;
|
|
7
|
+
}
|
|
8
|
+
implementationReportBlocker(feature, assignment) {
|
|
9
|
+
if (!assignment || assignment.status !== "submitted" || !assignment.reportSha256) {
|
|
10
|
+
return "Review is blocked because no submitted implementation report is registered.";
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const currentDigest = this.reportEvidence.readReportDigest(feature, assignment.report);
|
|
14
|
+
if (currentDigest !== assignment.reportSha256) {
|
|
15
|
+
return "The registered implementation report content changed after submission.";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return "The registered implementation report is missing or unavailable.";
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
resumeBlocker(feature, progress) {
|
|
24
|
+
if (progress.execution === "claimed" && (!progress.owner || !progress.baseSha)) {
|
|
25
|
+
return "Claim has no recorded owner or baseline; implementation is unavailable.";
|
|
26
|
+
}
|
|
27
|
+
if (progress.execution === "implementing" && !progress.assignment) {
|
|
28
|
+
return "Implementation is marked in progress without a recorded assignment.";
|
|
29
|
+
}
|
|
30
|
+
if (progress.execution === "reviewing" ||
|
|
31
|
+
(progress.execution === "implementing" && progress.assignment?.status === "submitted")) {
|
|
32
|
+
return this.implementationReportBlocker(feature, progress.assignment);
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Service } from "../../Foundation/Service.js";
|
|
2
|
+
export class CheckPolicy extends Service {
|
|
3
|
+
gitEvidence;
|
|
4
|
+
constructor(app, gitEvidence) {
|
|
5
|
+
super(app);
|
|
6
|
+
this.gitEvidence = gitEvidence;
|
|
7
|
+
}
|
|
8
|
+
hasCurrentCheckCoverage(feature, ticket, progress) {
|
|
9
|
+
const identity = this.gitEvidence.contentIdentity(feature, ticket, progress.baseSha, progress.checks);
|
|
10
|
+
return this.checkBlockers(feature, ticket, progress, identity).length === 0;
|
|
11
|
+
}
|
|
12
|
+
checkBlockers(feature, ticket, progress, identity) {
|
|
13
|
+
const checks = progress.checks;
|
|
14
|
+
if (!checks)
|
|
15
|
+
return [];
|
|
16
|
+
return checks.required
|
|
17
|
+
.filter((requirement) => !requirement.removedAt)
|
|
18
|
+
.flatMap((requirement) => {
|
|
19
|
+
const records = checks.records.filter((record) => record.command === requirement.command);
|
|
20
|
+
const preservedIntegrationEvidence = this.gitEvidence.integrationPreservesEvidence(progress, identity);
|
|
21
|
+
const current = records.filter((record) => {
|
|
22
|
+
if (record.snapshot === identity.snapshot &&
|
|
23
|
+
record.codeSnapshot === identity.codeSnapshot &&
|
|
24
|
+
record.requirementsSnapshot === identity.requirementsSnapshot) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
if (preservedIntegrationEvidence &&
|
|
28
|
+
record.snapshot === progress.acceptedEvidence?.snapshot &&
|
|
29
|
+
record.codeSnapshot === progress.acceptedEvidence?.codeSnapshot &&
|
|
30
|
+
record.requirementsSnapshot === progress.acceptedEvidence?.requirementsSnapshot) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
if (!record.coveredPaths ||
|
|
34
|
+
!record.coverageSnapshot ||
|
|
35
|
+
record.requirementsSnapshot !== identity.requirementsSnapshot) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const scopedIdentity = this.gitEvidence.contentIdentity(feature, ticket, progress.baseSha, checks, record.coveredPaths);
|
|
40
|
+
return scopedIdentity.codeSnapshot === record.coverageSnapshot;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
const latest = current.at(-1);
|
|
47
|
+
if (!latest) {
|
|
48
|
+
const state = records.length > 0 ? "stale" : "missing";
|
|
49
|
+
return [`Required check ${requirement.command} is ${state} for the current content.`];
|
|
50
|
+
}
|
|
51
|
+
if (latest.result !== "passed") {
|
|
52
|
+
const suffix = latest.details ? ` Details: ${latest.details}` : "";
|
|
53
|
+
return [`Required check ${requirement.command} is ${latest.result}.${suffix}`];
|
|
54
|
+
}
|
|
55
|
+
return [];
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
reviewCheckBlockers(feature, ticket, progress, identity) {
|
|
59
|
+
const activeRequirements = progress.checks?.required.filter((requirement) => !requirement.removedAt) ?? [];
|
|
60
|
+
if (activeRequirements.length === 0) {
|
|
61
|
+
return ["Review requires at least one currently required passing check."];
|
|
62
|
+
}
|
|
63
|
+
return this.checkBlockers(feature, ticket, progress, identity);
|
|
64
|
+
}
|
|
65
|
+
deliveryCheckBlockers(delivery, identity) {
|
|
66
|
+
const records = delivery.checks?.records ?? [];
|
|
67
|
+
const hasPassingCheck = records.some((record) => record.result === "passed" &&
|
|
68
|
+
record.snapshot === identity.snapshot &&
|
|
69
|
+
record.codeSnapshot === identity.codeSnapshot &&
|
|
70
|
+
record.requirementsSnapshot === identity.requirementsSnapshot);
|
|
71
|
+
const evidenceBlockers = hasPassingCheck
|
|
72
|
+
? []
|
|
73
|
+
: ["At least one relevant test/check must pass for the current feature snapshot."];
|
|
74
|
+
return [
|
|
75
|
+
...evidenceBlockers,
|
|
76
|
+
...(delivery.checks?.required ?? [])
|
|
77
|
+
.filter((requirement) => !requirement.removedAt)
|
|
78
|
+
.flatMap((requirement) => {
|
|
79
|
+
const matchingRecords = records.filter((record) => record.command === requirement.command &&
|
|
80
|
+
record.snapshot === identity.snapshot &&
|
|
81
|
+
record.codeSnapshot === identity.codeSnapshot &&
|
|
82
|
+
record.requirementsSnapshot === identity.requirementsSnapshot);
|
|
83
|
+
const latest = matchingRecords.at(-1);
|
|
84
|
+
if (!latest)
|
|
85
|
+
return [`Required delivery check ${requirement.command} is missing or stale.`];
|
|
86
|
+
if (latest.result !== "passed") {
|
|
87
|
+
return [`Required delivery check ${requirement.command} is ${latest.result}.`];
|
|
88
|
+
}
|
|
89
|
+
return [];
|
|
90
|
+
}),
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
}
|