agent-sdd 1.0.3
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 +1028 -0
- package/README.ru.md +1046 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +867 -0
- package/dist/features/approve/adapters/inbound/CliApproveHandler.d.ts +17 -0
- package/dist/features/approve/adapters/inbound/CliApproveHandler.js +108 -0
- package/dist/features/approve/adapters/outbound/NodeApproveFileSystem.d.ts +8 -0
- package/dist/features/approve/adapters/outbound/NodeApproveFileSystem.js +147 -0
- package/dist/features/approve/adapters/outbound/NodePlanFileWriter.d.ts +6 -0
- package/dist/features/approve/adapters/outbound/NodePlanFileWriter.js +92 -0
- package/dist/features/approve/adapters/outbound/SystemApproveClock.d.ts +4 -0
- package/dist/features/approve/adapters/outbound/SystemApproveClock.js +5 -0
- package/dist/features/approve/application/ApplyApproval.d.ts +19 -0
- package/dist/features/approve/application/ApplyApproval.js +30 -0
- package/dist/features/approve/application/WriteAttestation.d.ts +19 -0
- package/dist/features/approve/application/WriteAttestation.js +23 -0
- package/dist/features/approve/domain/ApproveRequest.d.ts +24 -0
- package/dist/features/approve/domain/ApproveRequest.js +24 -0
- package/dist/features/approve/domain/Rewrite.d.ts +1 -0
- package/dist/features/approve/domain/Rewrite.js +6 -0
- package/dist/features/approve/ports/inbound/ApproveCommand.d.ts +10 -0
- package/dist/features/approve/ports/inbound/ApproveCommand.js +1 -0
- package/dist/features/approve/ports/outbound/ApproveClock.d.ts +3 -0
- package/dist/features/approve/ports/outbound/ApproveClock.js +1 -0
- package/dist/features/approve/ports/outbound/ApproveConfigPort.d.ts +4 -0
- package/dist/features/approve/ports/outbound/ApproveConfigPort.js +1 -0
- package/dist/features/approve/ports/outbound/ApproveFileSystem.d.ts +8 -0
- package/dist/features/approve/ports/outbound/ApproveFileSystem.js +1 -0
- package/dist/features/approve/ports/outbound/PlanFileWriter.d.ts +13 -0
- package/dist/features/approve/ports/outbound/PlanFileWriter.js +1 -0
- package/dist/features/check/adapters/inbound/CliCheckHandler.d.ts +8 -0
- package/dist/features/check/adapters/inbound/CliCheckHandler.js +62 -0
- package/dist/features/check/adapters/outbound/ChildProcessCheckGit.d.ts +8 -0
- package/dist/features/check/adapters/outbound/ChildProcessCheckGit.js +112 -0
- package/dist/features/check/adapters/outbound/NodeCheckFileReader.d.ts +7 -0
- package/dist/features/check/adapters/outbound/NodeCheckFileReader.js +44 -0
- package/dist/features/check/application/CheckBaseline.d.ts +28 -0
- package/dist/features/check/application/CheckBaseline.js +54 -0
- package/dist/features/check/domain/BaselineComparison.d.ts +1 -0
- package/dist/features/check/domain/BaselineComparison.js +2 -0
- package/dist/features/check/ports/inbound/CheckCommand.d.ts +4 -0
- package/dist/features/check/ports/inbound/CheckCommand.js +1 -0
- package/dist/features/check/ports/outbound/CheckConfigPort.d.ts +4 -0
- package/dist/features/check/ports/outbound/CheckConfigPort.js +1 -0
- package/dist/features/check/ports/outbound/CheckGitPort.d.ts +7 -0
- package/dist/features/check/ports/outbound/CheckGitPort.js +1 -0
- package/dist/features/check/ports/outbound/CheckSpecPort.d.ts +9 -0
- package/dist/features/check/ports/outbound/CheckSpecPort.js +1 -0
- package/dist/features/doctor/adapters/inbound/CliDoctorHandler.d.ts +8 -0
- package/dist/features/doctor/adapters/inbound/CliDoctorHandler.js +77 -0
- package/dist/features/doctor/adapters/outbound/NodeRegistryReader.d.ts +5 -0
- package/dist/features/doctor/adapters/outbound/NodeRegistryReader.js +40 -0
- package/dist/features/doctor/application/RunDoctor.d.ts +30 -0
- package/dist/features/doctor/application/RunDoctor.js +78 -0
- package/dist/features/doctor/domain/RegistryRow.d.ts +23 -0
- package/dist/features/doctor/domain/RegistryRow.js +114 -0
- package/dist/features/doctor/domain/SemverRange.d.ts +7 -0
- package/dist/features/doctor/domain/SemverRange.js +82 -0
- package/dist/features/doctor/ports/inbound/DoctorCommand.d.ts +8 -0
- package/dist/features/doctor/ports/inbound/DoctorCommand.js +1 -0
- package/dist/features/doctor/ports/outbound/RegistryReader.d.ts +12 -0
- package/dist/features/doctor/ports/outbound/RegistryReader.js +1 -0
- package/dist/features/finalize/adapters/inbound/CliFinalizeHandler.d.ts +8 -0
- package/dist/features/finalize/adapters/inbound/CliFinalizeHandler.js +80 -0
- package/dist/features/finalize/adapters/outbound/NodeFinalizeFileSystem.d.ts +11 -0
- package/dist/features/finalize/adapters/outbound/NodeFinalizeFileSystem.js +167 -0
- package/dist/features/finalize/adapters/outbound/NodePlanRepo.d.ts +7 -0
- package/dist/features/finalize/adapters/outbound/NodePlanRepo.js +82 -0
- package/dist/features/finalize/adapters/outbound/SystemFinalizeClock.d.ts +4 -0
- package/dist/features/finalize/adapters/outbound/SystemFinalizeClock.js +5 -0
- package/dist/features/finalize/application/RunFinalize.d.ts +34 -0
- package/dist/features/finalize/application/RunFinalize.js +98 -0
- package/dist/features/finalize/domain/ValidateFinalizeGraph.d.ts +9 -0
- package/dist/features/finalize/domain/ValidateFinalizeGraph.js +86 -0
- package/dist/features/finalize/ports/inbound/FinalizeCommand.d.ts +7 -0
- package/dist/features/finalize/ports/inbound/FinalizeCommand.js +1 -0
- package/dist/features/finalize/ports/outbound/FinalizeClock.d.ts +3 -0
- package/dist/features/finalize/ports/outbound/FinalizeClock.js +1 -0
- package/dist/features/finalize/ports/outbound/FinalizeConfigPort.d.ts +4 -0
- package/dist/features/finalize/ports/outbound/FinalizeConfigPort.js +1 -0
- package/dist/features/finalize/ports/outbound/FinalizeFileSystem.d.ts +14 -0
- package/dist/features/finalize/ports/outbound/FinalizeFileSystem.js +1 -0
- package/dist/features/finalize/ports/outbound/PlanRepo.d.ts +21 -0
- package/dist/features/finalize/ports/outbound/PlanRepo.js +1 -0
- package/dist/features/install/adapters/inbound/CliInstallHandler.d.ts +8 -0
- package/dist/features/install/adapters/inbound/CliInstallHandler.js +54 -0
- package/dist/features/install/adapters/outbound/NodeInstallSource.d.ts +7 -0
- package/dist/features/install/adapters/outbound/NodeInstallSource.js +24 -0
- package/dist/features/install/adapters/outbound/NodeInstallTargetFs.d.ts +7 -0
- package/dist/features/install/adapters/outbound/NodeInstallTargetFs.js +30 -0
- package/dist/features/install/application/InstallRules.d.ts +10 -0
- package/dist/features/install/application/InstallRules.js +73 -0
- package/dist/features/install/domain/InstallPlan.d.ts +27 -0
- package/dist/features/install/domain/InstallPlan.js +168 -0
- package/dist/features/install/domain/InstallResult.d.ts +23 -0
- package/dist/features/install/domain/InstallResult.js +1 -0
- package/dist/features/install/domain/InstallTarget.d.ts +6 -0
- package/dist/features/install/domain/InstallTarget.js +7 -0
- package/dist/features/install/domain/ManagedBlock.d.ts +3 -0
- package/dist/features/install/domain/ManagedBlock.js +20 -0
- package/dist/features/install/domain/RuleManifest.d.ts +17 -0
- package/dist/features/install/domain/RuleManifest.js +69 -0
- package/dist/features/install/domain/SettingsMerge.d.ts +5 -0
- package/dist/features/install/domain/SettingsMerge.js +43 -0
- package/dist/features/install/ports/inbound/InstallCommand.d.ts +10 -0
- package/dist/features/install/ports/inbound/InstallCommand.js +1 -0
- package/dist/features/install/ports/outbound/InstallSource.d.ts +4 -0
- package/dist/features/install/ports/outbound/InstallSource.js +1 -0
- package/dist/features/install/ports/outbound/InstallTargetFs.d.ts +6 -0
- package/dist/features/install/ports/outbound/InstallTargetFs.js +1 -0
- package/dist/features/lint/adapters/inbound/CliLintHandler.d.ts +8 -0
- package/dist/features/lint/adapters/inbound/CliLintHandler.js +61 -0
- package/dist/features/lint/adapters/outbound/NodeLintFileReader.d.ts +7 -0
- package/dist/features/lint/adapters/outbound/NodeLintFileReader.js +165 -0
- package/dist/features/lint/application/RunLint.d.ts +10 -0
- package/dist/features/lint/application/RunLint.js +100 -0
- package/dist/features/lint/domain/Diagnostic.d.ts +1 -0
- package/dist/features/lint/domain/Diagnostic.js +2 -0
- package/dist/features/lint/domain/Record.d.ts +1 -0
- package/dist/features/lint/domain/Record.js +5 -0
- package/dist/features/lint/domain/Rules.d.ts +1 -0
- package/dist/features/lint/domain/Rules.js +2 -0
- package/dist/features/lint/domain/SpecParser.d.ts +1 -0
- package/dist/features/lint/domain/SpecParser.js +2 -0
- package/dist/features/lint/ports/inbound/LintCommand.d.ts +4 -0
- package/dist/features/lint/ports/inbound/LintCommand.js +1 -0
- package/dist/features/lint/ports/outbound/LintConfigPort.d.ts +4 -0
- package/dist/features/lint/ports/outbound/LintConfigPort.js +1 -0
- package/dist/features/lint/ports/outbound/LintFileReader.d.ts +10 -0
- package/dist/features/lint/ports/outbound/LintFileReader.js +1 -0
- package/dist/features/plan/adapters/inbound/CliPlanShowHandler.d.ts +8 -0
- package/dist/features/plan/adapters/inbound/CliPlanShowHandler.js +73 -0
- package/dist/features/plan/adapters/outbound/NodePlanReader.d.ts +7 -0
- package/dist/features/plan/adapters/outbound/NodePlanReader.js +68 -0
- package/dist/features/plan/application/ShowPlan.d.ts +7 -0
- package/dist/features/plan/application/ShowPlan.js +4 -0
- package/dist/features/plan/ports/inbound/PlanShowCommand.d.ts +7 -0
- package/dist/features/plan/ports/inbound/PlanShowCommand.js +1 -0
- package/dist/features/plan/ports/outbound/PlanConfigPort.d.ts +4 -0
- package/dist/features/plan/ports/outbound/PlanConfigPort.js +1 -0
- package/dist/features/plan/ports/outbound/PlanReader.d.ts +19 -0
- package/dist/features/plan/ports/outbound/PlanReader.js +1 -0
- package/dist/features/ready/adapters/inbound/CliReadyHandler.d.ts +8 -0
- package/dist/features/ready/adapters/inbound/CliReadyHandler.js +79 -0
- package/dist/features/ready/adapters/outbound/ChildProcessReadyGit.d.ts +9 -0
- package/dist/features/ready/adapters/outbound/ChildProcessReadyGit.js +113 -0
- package/dist/features/ready/adapters/outbound/NodeReadyFileSystem.d.ts +8 -0
- package/dist/features/ready/adapters/outbound/NodeReadyFileSystem.js +159 -0
- package/dist/features/ready/application/RunReady.d.ts +16 -0
- package/dist/features/ready/application/RunReady.js +572 -0
- package/dist/features/ready/domain/AggregatedRules.d.ts +16 -0
- package/dist/features/ready/domain/AggregatedRules.js +42 -0
- package/dist/features/ready/domain/MarkerParser.d.ts +17 -0
- package/dist/features/ready/domain/MarkerParser.js +108 -0
- package/dist/features/ready/domain/PartitionResolver.d.ts +1 -0
- package/dist/features/ready/domain/PartitionResolver.js +5 -0
- package/dist/features/ready/domain/ReadyInput.d.ts +6 -0
- package/dist/features/ready/domain/ReadyInput.js +1 -0
- package/dist/features/ready/domain/ReadyViolation.d.ts +38 -0
- package/dist/features/ready/domain/ReadyViolation.js +19 -0
- package/dist/features/ready/domain/Rules.d.ts +22 -0
- package/dist/features/ready/domain/Rules.js +243 -0
- package/dist/features/ready/domain/SpecDiff.d.ts +33 -0
- package/dist/features/ready/domain/SpecDiff.js +321 -0
- package/dist/features/ready/ports/inbound/ReadyCommand.d.ts +4 -0
- package/dist/features/ready/ports/inbound/ReadyCommand.js +1 -0
- package/dist/features/ready/ports/outbound/ReadyConfigPort.d.ts +4 -0
- package/dist/features/ready/ports/outbound/ReadyConfigPort.js +1 -0
- package/dist/features/ready/ports/outbound/ReadyFileReader.d.ts +12 -0
- package/dist/features/ready/ports/outbound/ReadyFileReader.js +1 -0
- package/dist/features/ready/ports/outbound/ReadyGitPort.d.ts +10 -0
- package/dist/features/ready/ports/outbound/ReadyGitPort.js +5 -0
- package/dist/features/record/adapters/inbound/CliRecordHandler.d.ts +10 -0
- package/dist/features/record/adapters/inbound/CliRecordHandler.js +111 -0
- package/dist/features/record/adapters/outbound/NodeRecordFileSystem.d.ts +9 -0
- package/dist/features/record/adapters/outbound/NodeRecordFileSystem.js +152 -0
- package/dist/features/record/application/AddRecord.d.ts +11 -0
- package/dist/features/record/application/AddRecord.js +84 -0
- package/dist/features/record/application/GetRecord.d.ts +8 -0
- package/dist/features/record/application/GetRecord.js +22 -0
- package/dist/features/record/application/ListRecords.d.ts +9 -0
- package/dist/features/record/application/ListRecords.js +24 -0
- package/dist/features/record/application/SetRecord.d.ts +11 -0
- package/dist/features/record/application/SetRecord.js +68 -0
- package/dist/features/record/domain/RecordBody.d.ts +12 -0
- package/dist/features/record/domain/RecordBody.js +66 -0
- package/dist/features/record/domain/RecordPartition.d.ts +1 -0
- package/dist/features/record/domain/RecordPartition.js +7 -0
- package/dist/features/record/domain/RecordSlice.d.ts +7 -0
- package/dist/features/record/domain/RecordSlice.js +1 -0
- package/dist/features/record/domain/RecordSummary.d.ts +11 -0
- package/dist/features/record/domain/RecordSummary.js +13 -0
- package/dist/features/record/domain/RecordWrite.d.ts +14 -0
- package/dist/features/record/domain/RecordWrite.js +8 -0
- package/dist/features/record/ports/inbound/RecordCommand.d.ts +19 -0
- package/dist/features/record/ports/inbound/RecordCommand.js +1 -0
- package/dist/features/record/ports/outbound/RecordConfigPort.d.ts +4 -0
- package/dist/features/record/ports/outbound/RecordConfigPort.js +1 -0
- package/dist/features/record/ports/outbound/RecordFileReader.d.ts +10 -0
- package/dist/features/record/ports/outbound/RecordFileReader.js +1 -0
- package/dist/features/record/ports/outbound/RecordFileWriter.d.ts +6 -0
- package/dist/features/record/ports/outbound/RecordFileWriter.js +1 -0
- package/dist/features/refresh/adapters/inbound/CliRefreshHandler.d.ts +8 -0
- package/dist/features/refresh/adapters/inbound/CliRefreshHandler.js +24 -0
- package/dist/features/refresh/adapters/outbound/ChildProcessRefreshGit.d.ts +8 -0
- package/dist/features/refresh/adapters/outbound/ChildProcessRefreshGit.js +118 -0
- package/dist/features/refresh/adapters/outbound/NodeRefreshFileReader.d.ts +7 -0
- package/dist/features/refresh/adapters/outbound/NodeRefreshFileReader.js +44 -0
- package/dist/features/refresh/adapters/outbound/SystemRefreshClock.d.ts +4 -0
- package/dist/features/refresh/adapters/outbound/SystemRefreshClock.js +5 -0
- package/dist/features/refresh/application/BuildRefreshStubs.d.ts +25 -0
- package/dist/features/refresh/application/BuildRefreshStubs.js +43 -0
- package/dist/features/refresh/domain/DiffStubs.d.ts +24 -0
- package/dist/features/refresh/domain/DiffStubs.js +81 -0
- package/dist/features/refresh/domain/Footprint.d.ts +14 -0
- package/dist/features/refresh/domain/Footprint.js +45 -0
- package/dist/features/refresh/ports/inbound/RefreshCommand.d.ts +4 -0
- package/dist/features/refresh/ports/inbound/RefreshCommand.js +1 -0
- package/dist/features/refresh/ports/outbound/RefreshClockPort.d.ts +3 -0
- package/dist/features/refresh/ports/outbound/RefreshClockPort.js +1 -0
- package/dist/features/refresh/ports/outbound/RefreshConfigPort.d.ts +4 -0
- package/dist/features/refresh/ports/outbound/RefreshConfigPort.js +1 -0
- package/dist/features/refresh/ports/outbound/RefreshGitPort.d.ts +7 -0
- package/dist/features/refresh/ports/outbound/RefreshGitPort.js +1 -0
- package/dist/features/refresh/ports/outbound/RefreshSpecPort.d.ts +9 -0
- package/dist/features/refresh/ports/outbound/RefreshSpecPort.js +1 -0
- package/dist/features/report/adapters/inbound/CliReportHandler.d.ts +8 -0
- package/dist/features/report/adapters/inbound/CliReportHandler.js +35 -0
- package/dist/features/report/adapters/outbound/NodeReportFileSystem.d.ts +7 -0
- package/dist/features/report/adapters/outbound/NodeReportFileSystem.js +128 -0
- package/dist/features/report/application/RunReport.d.ts +19 -0
- package/dist/features/report/application/RunReport.js +161 -0
- package/dist/features/report/ports/inbound/ReportCommand.d.ts +8 -0
- package/dist/features/report/ports/inbound/ReportCommand.js +1 -0
- package/dist/features/report/ports/outbound/ReportConfigPort.d.ts +4 -0
- package/dist/features/report/ports/outbound/ReportConfigPort.js +1 -0
- package/dist/features/report/ports/outbound/ReportFileReader.d.ts +7 -0
- package/dist/features/report/ports/outbound/ReportFileReader.js +1 -0
- package/dist/features/token/adapters/inbound/CliTokenHandler.d.ts +8 -0
- package/dist/features/token/adapters/inbound/CliTokenHandler.js +53 -0
- package/dist/features/token/adapters/outbound/ChildProcessTokenGit.d.ts +8 -0
- package/dist/features/token/adapters/outbound/ChildProcessTokenGit.js +112 -0
- package/dist/features/token/adapters/outbound/NodeTokenConfigReader.d.ts +5 -0
- package/dist/features/token/adapters/outbound/NodeTokenConfigReader.js +41 -0
- package/dist/features/token/application/ComputeToken.d.ts +18 -0
- package/dist/features/token/application/ComputeToken.js +27 -0
- package/dist/features/token/ports/inbound/TokenCommand.d.ts +4 -0
- package/dist/features/token/ports/inbound/TokenCommand.js +1 -0
- package/dist/features/token/ports/outbound/TokenConfigPort.d.ts +4 -0
- package/dist/features/token/ports/outbound/TokenConfigPort.js +1 -0
- package/dist/features/token/ports/outbound/TokenGitPort.d.ts +7 -0
- package/dist/features/token/ports/outbound/TokenGitPort.js +1 -0
- package/dist/shared/domain/AgentBlocklist.d.ts +5 -0
- package/dist/shared/domain/AgentBlocklist.js +28 -0
- package/dist/shared/domain/BoundaryReachability.d.ts +5 -0
- package/dist/shared/domain/BoundaryReachability.js +71 -0
- package/dist/shared/domain/CheckOutcome.d.ts +10 -0
- package/dist/shared/domain/CheckOutcome.js +7 -0
- package/dist/shared/domain/CliOutput.d.ts +10 -0
- package/dist/shared/domain/CliOutput.js +29 -0
- package/dist/shared/domain/Config.d.ts +29 -0
- package/dist/shared/domain/Config.js +201 -0
- package/dist/shared/domain/DiagnosticRegistry.d.ts +8 -0
- package/dist/shared/domain/DiagnosticRegistry.js +71 -0
- package/dist/shared/domain/Errors.d.ts +12 -0
- package/dist/shared/domain/Errors.js +23 -0
- package/dist/shared/domain/GlobMatch.d.ts +2 -0
- package/dist/shared/domain/GlobMatch.js +58 -0
- package/dist/shared/domain/LintReport.d.ts +16 -0
- package/dist/shared/domain/LintReport.js +11 -0
- package/dist/shared/domain/LintRules.d.ts +67 -0
- package/dist/shared/domain/LintRules.js +956 -0
- package/dist/shared/domain/PartitionGrammar.d.ts +4 -0
- package/dist/shared/domain/PartitionGrammar.js +16 -0
- package/dist/shared/domain/PlanFile.d.ts +28 -0
- package/dist/shared/domain/PlanFile.js +112 -0
- package/dist/shared/domain/Scope.d.ts +1 -0
- package/dist/shared/domain/Scope.js +3 -0
- package/dist/shared/domain/SpecApprovalRewrite.d.ts +23 -0
- package/dist/shared/domain/SpecApprovalRewrite.js +254 -0
- package/dist/shared/domain/SpecBlocks.d.ts +12 -0
- package/dist/shared/domain/SpecBlocks.js +96 -0
- package/dist/shared/domain/SpecRecord.d.ts +17 -0
- package/dist/shared/domain/SpecRecord.js +208 -0
- package/dist/shared/domain/TemplateFieldMetadata.d.ts +2 -0
- package/dist/shared/domain/TemplateFieldMetadata.js +177 -0
- package/dist/shared/domain/Token.d.ts +2 -0
- package/dist/shared/domain/Token.js +5 -0
- package/dist/shared/domain/WeaselWords.d.ts +3 -0
- package/dist/shared/domain/WeaselWords.js +32 -0
- package/package.json +71 -0
- package/rules/enforcement_registry.md +126 -0
- package/rules/hooks/sdd-lint-reminder.sh +33 -0
- package/rules/hooks/sdd-spec-read-guard.sh +73 -0
- package/rules/manifest.json +15 -0
- package/rules/review-sdd.md +9 -0
- package/rules/sdd-cli-usage.md +91 -0
- package/rules/skills/spec-driven-development/SKILL.md +554 -0
- package/rules/skills/spec-driven-development/data/weasel-words.json +22 -0
- package/rules/spec-driven-development.md +69 -0
- package/rules/tdd-sdd.md +127 -0
- package/rules/workflow-sdd.md +56 -0
- package/schema/sdd.config.schema.json +104 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ENF-004A — Semver cascade diff engine. Classifies per-ID changes and
|
|
3
|
+
* computes the required Surface bump (rationale lives in the spec record).
|
|
4
|
+
* Pure logic — no I/O.
|
|
5
|
+
*/
|
|
6
|
+
const PREDICATE_FIELDS = new Set([
|
|
7
|
+
"always",
|
|
8
|
+
"never",
|
|
9
|
+
"when",
|
|
10
|
+
"then",
|
|
11
|
+
"predicate",
|
|
12
|
+
"schema",
|
|
13
|
+
"preconditions",
|
|
14
|
+
"postconditions",
|
|
15
|
+
"error_taxonomy",
|
|
16
|
+
"compatibility_rules",
|
|
17
|
+
"rule" /* Constraint, Policy */,
|
|
18
|
+
"metric" /* NFR */,
|
|
19
|
+
"target" /* NFR */,
|
|
20
|
+
"given" /* Behavior */,
|
|
21
|
+
]);
|
|
22
|
+
/** Per-ID diff: classify each ID present in `curr` against `prev`. IDs new to
|
|
23
|
+
* `curr` are classified as "content_change" (additive). IDs removed in
|
|
24
|
+
* `curr` are not returned (the consumer detects removal separately via
|
|
25
|
+
* lifecycle.status flips, which lint already covers). */
|
|
26
|
+
export function classifyDiff(prev, curr) {
|
|
27
|
+
const prevById = new Map(prev.map((r) => [r.id, r]));
|
|
28
|
+
const out = [];
|
|
29
|
+
for (const c of curr) {
|
|
30
|
+
const p = prevById.get(c.id);
|
|
31
|
+
if (p === undefined) {
|
|
32
|
+
out.push({
|
|
33
|
+
id: c.id,
|
|
34
|
+
template: c.template,
|
|
35
|
+
classification: "content_change",
|
|
36
|
+
changedFields: ["__new__"],
|
|
37
|
+
});
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const changed = changedTopLevelKeys(p.parsed, c.parsed);
|
|
41
|
+
if (changed.length === 0) {
|
|
42
|
+
out.push({
|
|
43
|
+
id: c.id,
|
|
44
|
+
template: c.template,
|
|
45
|
+
classification: "none",
|
|
46
|
+
changedFields: [],
|
|
47
|
+
});
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const isPredicate = changed.some((k) => {
|
|
51
|
+
/*
|
|
52
|
+
* CTR-016 / CTR-012: append-only schema additions are content, not
|
|
53
|
+
* predicate (see spec record for the full rule).
|
|
54
|
+
*/
|
|
55
|
+
if (k === "schema" &&
|
|
56
|
+
isAppendOnlySchemaChange(p.parsed.schema, c.parsed.schema)) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
return PREDICATE_FIELDS.has(k);
|
|
60
|
+
});
|
|
61
|
+
out.push({
|
|
62
|
+
id: c.id,
|
|
63
|
+
template: c.template,
|
|
64
|
+
classification: isPredicate ? "predicate_change" : "content_change",
|
|
65
|
+
changedFields: changed,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
export function requiredSurfaceBumps(prev, curr, diffs) {
|
|
71
|
+
const prevById = new Map(prev.map((r) => [r.id, r]));
|
|
72
|
+
const diffById = new Map(diffs.map((d) => [d.id, d]));
|
|
73
|
+
const currSurfaces = curr.filter((r) => r.template === "Surface");
|
|
74
|
+
const out = [];
|
|
75
|
+
for (const sur of currSurfaces) {
|
|
76
|
+
const reachable = reachableFrom(sur, curr);
|
|
77
|
+
const drivenBy = [];
|
|
78
|
+
for (const id of reachable) {
|
|
79
|
+
const d = diffById.get(id);
|
|
80
|
+
if (d !== undefined && d.classification !== "none") {
|
|
81
|
+
drivenBy.push(d);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const required = drivenBy.some((d) => d.classification === "predicate_change")
|
|
85
|
+
? "major"
|
|
86
|
+
: drivenBy.some((d) => d.classification === "content_change")
|
|
87
|
+
? "minor"
|
|
88
|
+
: "patch";
|
|
89
|
+
const declaredVersion = readVersion(sur);
|
|
90
|
+
const prevSur = prevById.get(sur.id);
|
|
91
|
+
const prevDeclaredVersion = prevSur !== undefined ? readVersion(prevSur) : null;
|
|
92
|
+
out.push({
|
|
93
|
+
surfaceId: sur.id,
|
|
94
|
+
declaredVersion,
|
|
95
|
+
prevDeclaredVersion,
|
|
96
|
+
required,
|
|
97
|
+
drivenBy,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
function reachableFrom(surface, all) {
|
|
103
|
+
const out = new Set([surface.id]);
|
|
104
|
+
const byId = new Map(all.map((r) => [r.id, r]));
|
|
105
|
+
const queue = [surface.id];
|
|
106
|
+
while (queue.length > 0) {
|
|
107
|
+
const id = queue[0];
|
|
108
|
+
queue.shift();
|
|
109
|
+
const r = byId.get(id);
|
|
110
|
+
if (r === undefined) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const members = r.parsed.members;
|
|
114
|
+
if (Array.isArray(members)) {
|
|
115
|
+
for (const m of members) {
|
|
116
|
+
if (typeof m === "string" && !out.has(m)) {
|
|
117
|
+
out.add(m);
|
|
118
|
+
queue.push(m);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const policyRefs = r.parsed.policy_refs;
|
|
123
|
+
if (Array.isArray(policyRefs)) {
|
|
124
|
+
for (const p of policyRefs) {
|
|
125
|
+
if (typeof p === "string" && !out.has(p)) {
|
|
126
|
+
out.add(p);
|
|
127
|
+
queue.push(p);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
function readVersion(rec) {
|
|
135
|
+
const v = rec.parsed.version;
|
|
136
|
+
return typeof v === "string" ? v : null;
|
|
137
|
+
}
|
|
138
|
+
export function generatedArtifactStructuralDiffs(prev, curr, diffs) {
|
|
139
|
+
const byId = new Map(curr.map((r) => [r.id, r]));
|
|
140
|
+
const diffById = new Map(diffs.map((d) => [d.id, d]));
|
|
141
|
+
void prev;
|
|
142
|
+
const out = [];
|
|
143
|
+
const surfaces = curr.filter((r) => r.template === "Surface");
|
|
144
|
+
for (const sur of surfaces) {
|
|
145
|
+
const reachable = reachableFrom(sur, curr);
|
|
146
|
+
for (const id of reachable) {
|
|
147
|
+
if (id === sur.id) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const rec = byId.get(id);
|
|
151
|
+
if (rec === undefined) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (rec.template !== "GeneratedArtifact") {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (rec.parsed.published_surface !== "yes") {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const d = diffById.get(id);
|
|
161
|
+
if (d === undefined || d.classification === "none") {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
out.push({
|
|
165
|
+
surfaceId: sur.id,
|
|
166
|
+
generatedArtifactId: id,
|
|
167
|
+
classification: d.classification,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
/** Compare two semver strings ("0.3.0" vs "0.4.0") and return the actual bump.
|
|
174
|
+
* Returns null if either string is unparseable. */
|
|
175
|
+
export function actualBump(prev, curr) {
|
|
176
|
+
if (prev === null || curr === null) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
const p = parseSemver(prev);
|
|
180
|
+
const c = parseSemver(curr);
|
|
181
|
+
if (p === null || c === null) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
if (c.major > p.major) {
|
|
185
|
+
return "major";
|
|
186
|
+
}
|
|
187
|
+
if (c.major === p.major && c.minor > p.minor) {
|
|
188
|
+
return "minor";
|
|
189
|
+
}
|
|
190
|
+
if (c.major === p.major && c.minor === p.minor && c.patch > p.patch) {
|
|
191
|
+
return "patch";
|
|
192
|
+
}
|
|
193
|
+
return "patch"; /* unchanged or downgrade — treat as patch (no cascade triggered) */
|
|
194
|
+
}
|
|
195
|
+
function parseSemver(v) {
|
|
196
|
+
const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(v.trim());
|
|
197
|
+
if (m === null) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
major: Number.parseInt(m[1], 10),
|
|
202
|
+
minor: Number.parseInt(m[2], 10),
|
|
203
|
+
patch: Number.parseInt(m[3], 10),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const BUMP_RANK = {
|
|
207
|
+
patch: 0,
|
|
208
|
+
minor: 1,
|
|
209
|
+
major: 2,
|
|
210
|
+
};
|
|
211
|
+
export function bumpAtLeast(actual, required) {
|
|
212
|
+
if (actual === null) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
return BUMP_RANK[actual] >= BUMP_RANK[required];
|
|
216
|
+
}
|
|
217
|
+
function isPlainObject(v) {
|
|
218
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
219
|
+
}
|
|
220
|
+
/* CTR-016 / CTR-012 / CTR-014 — schema sub-trees that are append-only at minor
|
|
221
|
+
* (see spec records). */
|
|
222
|
+
const APPEND_ONLY_ZONE_KEYS = new Set([
|
|
223
|
+
"members",
|
|
224
|
+
"fields",
|
|
225
|
+
"json",
|
|
226
|
+
]);
|
|
227
|
+
/* True when a `schema` change is confined to pure additions inside an
|
|
228
|
+
* append-only zone; any removal or modification stays a predicate change. */
|
|
229
|
+
function isAppendOnlySchemaChange(prev, curr) {
|
|
230
|
+
if (!isPlainObject(prev) || !isPlainObject(curr)) {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
return appendOnlyWithinZones(prev, curr, false) && !deepEqual(prev, curr);
|
|
234
|
+
}
|
|
235
|
+
function appendOnlyWithinZones(prev, curr, inZone) {
|
|
236
|
+
if (deepEqual(prev, curr)) {
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
if (Array.isArray(prev) && Array.isArray(curr)) {
|
|
240
|
+
if (!inZone) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
return prev.every((e) => curr.some((c) => deepEqual(c, e)));
|
|
244
|
+
}
|
|
245
|
+
if (isPlainObject(prev) && isPlainObject(curr)) {
|
|
246
|
+
if (inZone) {
|
|
247
|
+
return Object.keys(prev).every((k) => k in curr && appendOnlyWithinZones(prev[k], curr[k], true));
|
|
248
|
+
}
|
|
249
|
+
const prevKeys = Object.keys(prev);
|
|
250
|
+
const currKeys = Object.keys(curr);
|
|
251
|
+
if (prevKeys.length !== currKeys.length) {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
return prevKeys.every((k) => k in curr &&
|
|
255
|
+
appendOnlyWithinZones(prev[k], curr[k], APPEND_ONLY_ZONE_KEYS.has(k)));
|
|
256
|
+
}
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
function changedTopLevelKeys(prev, curr) {
|
|
260
|
+
const out = [];
|
|
261
|
+
const keys = new Set([...Object.keys(prev), ...Object.keys(curr)]);
|
|
262
|
+
/*
|
|
263
|
+
* Skip metadata keys that don't affect semver: id, type, partition_id,
|
|
264
|
+
* lifecycle (status flips are governed separately), approval_record.
|
|
265
|
+
*/
|
|
266
|
+
const SKIP = new Set([
|
|
267
|
+
"id",
|
|
268
|
+
"type",
|
|
269
|
+
"partition_id",
|
|
270
|
+
"lifecycle",
|
|
271
|
+
"approval_record",
|
|
272
|
+
"version",
|
|
273
|
+
]);
|
|
274
|
+
for (const k of keys) {
|
|
275
|
+
if (SKIP.has(k)) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (!deepEqual(prev[k], curr[k])) {
|
|
279
|
+
out.push(k);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return out;
|
|
283
|
+
}
|
|
284
|
+
function deepEqual(a, b) {
|
|
285
|
+
if (a === b) {
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
if (a === null || b === null) {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
if (typeof a !== typeof b) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
if (typeof a !== "object") {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
if (Array.isArray(a) !== Array.isArray(b)) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
301
|
+
if (a.length !== b.length) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
for (let i = 0; i < a.length; i++) {
|
|
305
|
+
if (!deepEqual(a[i], b[i])) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
if (!isPlainObject(a) || !isPlainObject(b)) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
315
|
+
for (const k of keys) {
|
|
316
|
+
if (!deepEqual(a[k], b[k])) {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface SpecFileEntry {
|
|
2
|
+
path: string;
|
|
3
|
+
content: string;
|
|
4
|
+
}
|
|
5
|
+
export interface TestFileEntry {
|
|
6
|
+
path: string;
|
|
7
|
+
content: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ReadyFileReader {
|
|
10
|
+
resolveSpecFiles(repoRoot: string, patterns: readonly string[]): Promise<SpecFileEntry[]>;
|
|
11
|
+
resolveTestFiles(repoRoot: string, patterns: readonly string[]): Promise<TestFileEntry[]>;
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface ReadyGitPort {
|
|
2
|
+
isGitRepo(cwd: string): Promise<boolean>;
|
|
3
|
+
repoRoot(cwd: string): Promise<string>;
|
|
4
|
+
treeBytes(repoRoot: string, scope: readonly string[]): Promise<Uint8Array>;
|
|
5
|
+
treePaths(repoRoot: string, scope: readonly string[]): Promise<string[]>;
|
|
6
|
+
dirtyPaths(repoRoot: string, scope: readonly string[]): Promise<string[]>;
|
|
7
|
+
/** Read a single file at a given ref (e.g. HEAD~5). Returns null if the
|
|
8
|
+
* ref or file is missing. Used by P2.3 semver-cascade diff. */
|
|
9
|
+
readAtRef(repoRoot: string, ref: string, relativePath: string): Promise<string | null>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type CommandResult, type OutputFormat } from "../../../../shared/domain/CliOutput.js";
|
|
2
|
+
import { type RecordWritePorts } from "../../application/SetRecord.js";
|
|
3
|
+
import type { RecordAction, RecordCommand } from "../../ports/inbound/RecordCommand.js";
|
|
4
|
+
type Format = Exclude<OutputFormat, "yaml">;
|
|
5
|
+
export declare class CliRecordHandler implements RecordCommand {
|
|
6
|
+
private readonly ports;
|
|
7
|
+
constructor(ports: RecordWritePorts);
|
|
8
|
+
execute(cwd: string, action: RecordAction, format: Format): Promise<CommandResult>;
|
|
9
|
+
}
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { ok, } from "../../../../shared/domain/CliOutput.js";
|
|
2
|
+
import { addRecord } from "../../application/AddRecord.js";
|
|
3
|
+
import { getRecord } from "../../application/GetRecord.js";
|
|
4
|
+
import { listRecords, } from "../../application/ListRecords.js";
|
|
5
|
+
import { setRecord, } from "../../application/SetRecord.js";
|
|
6
|
+
export class CliRecordHandler {
|
|
7
|
+
ports;
|
|
8
|
+
constructor(ports) {
|
|
9
|
+
this.ports = ports;
|
|
10
|
+
}
|
|
11
|
+
async execute(cwd, action, format) {
|
|
12
|
+
if (action.kind === "list") {
|
|
13
|
+
const records = await listRecords(cwd, this.ports, action.partition);
|
|
14
|
+
return format === "json" ? listJson(records) : listHuman(records);
|
|
15
|
+
}
|
|
16
|
+
if (action.kind === "get") {
|
|
17
|
+
const matches = await getRecord(cwd, action.id, this.ports);
|
|
18
|
+
return format === "json"
|
|
19
|
+
? getJson(action.id, matches)
|
|
20
|
+
: getHuman(action.id, matches);
|
|
21
|
+
}
|
|
22
|
+
const result = action.kind === "set"
|
|
23
|
+
? await setRecord(cwd, action.id, action.body, this.ports)
|
|
24
|
+
: await addRecord(cwd, action.afterId, action.body, this.ports);
|
|
25
|
+
return writeResult(result, format);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function listJson(records) {
|
|
29
|
+
return ok(JSON.stringify({
|
|
30
|
+
format_version: 1,
|
|
31
|
+
count: records.length,
|
|
32
|
+
records: records.map((r) => ({
|
|
33
|
+
id: r.id,
|
|
34
|
+
type: r.type,
|
|
35
|
+
status: r.status,
|
|
36
|
+
title: r.title,
|
|
37
|
+
file: r.file,
|
|
38
|
+
line: r.line,
|
|
39
|
+
})),
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
function listHuman(records) {
|
|
43
|
+
if (records.length === 0) {
|
|
44
|
+
return ok("");
|
|
45
|
+
}
|
|
46
|
+
const idW = Math.max(...records.map((r) => r.id.length));
|
|
47
|
+
const typeW = Math.max(...records.map((r) => (r.type ?? "—").length));
|
|
48
|
+
const statusW = Math.max(...records.map((r) => (r.status ?? "—").length));
|
|
49
|
+
const lines = records.map((r) => {
|
|
50
|
+
const id = r.id.padEnd(idW);
|
|
51
|
+
const type = (r.type ?? "—").padEnd(typeW);
|
|
52
|
+
const status = (r.status ?? "—").padEnd(statusW);
|
|
53
|
+
return `${id} ${type} ${status} ${r.title ?? ""}`.trimEnd();
|
|
54
|
+
});
|
|
55
|
+
return ok(lines.join("\n"));
|
|
56
|
+
}
|
|
57
|
+
function getJson(id, matches) {
|
|
58
|
+
const found = matches[0];
|
|
59
|
+
if (found === undefined) {
|
|
60
|
+
return {
|
|
61
|
+
exitCode: 1,
|
|
62
|
+
stdout: `${JSON.stringify({ format_version: 1, found: false, id })}\n`,
|
|
63
|
+
stderr: "",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return ok(JSON.stringify({
|
|
67
|
+
format_version: 1,
|
|
68
|
+
found: true,
|
|
69
|
+
id: found.id,
|
|
70
|
+
file: found.file,
|
|
71
|
+
start_line: found.startLine,
|
|
72
|
+
end_line: found.endLine,
|
|
73
|
+
raw: found.raw,
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
function getHuman(id, matches) {
|
|
77
|
+
if (matches.length === 0) {
|
|
78
|
+
return { exitCode: 1, stdout: "", stderr: `record not found: ${id}\n` };
|
|
79
|
+
}
|
|
80
|
+
return ok(matches.map((m) => m.raw).join("\n---\n"));
|
|
81
|
+
}
|
|
82
|
+
function writeResult(result, format) {
|
|
83
|
+
if (!result.ok) {
|
|
84
|
+
if (format === "json") {
|
|
85
|
+
const body = JSON.stringify({
|
|
86
|
+
format_version: 1,
|
|
87
|
+
ok: false,
|
|
88
|
+
reason: result.reason,
|
|
89
|
+
detail: result.message,
|
|
90
|
+
});
|
|
91
|
+
return { exitCode: result.exitCode, stdout: `${body}\n`, stderr: "" };
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
exitCode: result.exitCode,
|
|
95
|
+
stdout: "",
|
|
96
|
+
stderr: `${result.reason}: ${result.message}\n`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (format === "json") {
|
|
100
|
+
return ok(JSON.stringify({
|
|
101
|
+
format_version: 1,
|
|
102
|
+
ok: true,
|
|
103
|
+
action: result.action,
|
|
104
|
+
id: result.id,
|
|
105
|
+
file: result.file,
|
|
106
|
+
start_line: result.startLine,
|
|
107
|
+
end_line: result.endLine,
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
return ok(`record ${result.action}: ${result.id} (${result.file}:${result.startLine}-${result.endLine})`);
|
|
111
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type SddConfig } from "../../../../shared/domain/Config.js";
|
|
2
|
+
import type { RecordConfigPort } from "../../ports/outbound/RecordConfigPort.js";
|
|
3
|
+
import type { RecordFileReader, SpecFileEntry } from "../../ports/outbound/RecordFileReader.js";
|
|
4
|
+
import type { RecordFileWriter } from "../../ports/outbound/RecordFileWriter.js";
|
|
5
|
+
export declare class NodeRecordFileSystem implements RecordConfigPort, RecordFileReader, RecordFileWriter {
|
|
6
|
+
config(repoRoot: string): Promise<SddConfig>;
|
|
7
|
+
writeSpecFile(repoRoot: string, relativePath: string, content: string): Promise<void>;
|
|
8
|
+
resolveSpecFiles(repoRoot: string, patterns: readonly string[]): Promise<SpecFileEntry[]>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { readFile, readdir, rename, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join, relative, resolve } from "node:path";
|
|
3
|
+
import { configFromJson, } from "../../../../shared/domain/Config.js";
|
|
4
|
+
import { configFailure } from "../../../../shared/domain/Errors.js";
|
|
5
|
+
export class NodeRecordFileSystem {
|
|
6
|
+
async config(repoRoot) {
|
|
7
|
+
const configPath = join(repoRoot, ".sdd", "config.json");
|
|
8
|
+
const text = await readConfig(configPath);
|
|
9
|
+
return configFromJson(parseConfigJson(text, configPath), configPath);
|
|
10
|
+
}
|
|
11
|
+
async writeSpecFile(repoRoot, relativePath, content) {
|
|
12
|
+
const abs = resolve(repoRoot, relativePath);
|
|
13
|
+
const tmp = `${abs}.tmp.${process.pid}.${Date.now()}`;
|
|
14
|
+
await writeFile(tmp, content, "utf8");
|
|
15
|
+
await rename(tmp, abs);
|
|
16
|
+
}
|
|
17
|
+
async resolveSpecFiles(repoRoot, patterns) {
|
|
18
|
+
const matched = new Set();
|
|
19
|
+
for (const pattern of patterns) {
|
|
20
|
+
for (const abs of await expandGlob(repoRoot, pattern)) {
|
|
21
|
+
matched.add(abs);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const out = [];
|
|
25
|
+
for (const abs of [...matched].sort()) {
|
|
26
|
+
const text = await readFile(abs, "utf8");
|
|
27
|
+
const rel = relative(repoRoot, abs);
|
|
28
|
+
out.push({ path: rel.split("\\").join("/"), content: text });
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function expandGlob(repoRoot, pattern) {
|
|
34
|
+
const normalised = pattern.split("\\").join("/");
|
|
35
|
+
if (!hasGlob(normalised)) {
|
|
36
|
+
const abs = resolve(repoRoot, normalised);
|
|
37
|
+
return (await isFile(abs)) ? [abs] : [];
|
|
38
|
+
}
|
|
39
|
+
const segments = normalised.split("/");
|
|
40
|
+
const literalSegments = [];
|
|
41
|
+
let firstGlobIndex = -1;
|
|
42
|
+
for (let i = 0; i < segments.length; i++) {
|
|
43
|
+
if (hasGlob(segments[i])) {
|
|
44
|
+
firstGlobIndex = i;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
literalSegments.push(segments[i]);
|
|
48
|
+
}
|
|
49
|
+
const baseDir = resolve(repoRoot, ...literalSegments);
|
|
50
|
+
const out = [];
|
|
51
|
+
await walkAndMatch(baseDir, segments.slice(firstGlobIndex), out);
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
async function walkAndMatch(dir, remaining, acc) {
|
|
55
|
+
if (remaining.length === 0) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const head = remaining[0];
|
|
59
|
+
const rest = remaining.slice(1);
|
|
60
|
+
let entries;
|
|
61
|
+
try {
|
|
62
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (head === "**") {
|
|
68
|
+
if (rest.length === 0) {
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
const abs = join(dir, entry.name);
|
|
71
|
+
if (entry.isDirectory()) {
|
|
72
|
+
await walkAndMatch(abs, ["**"], acc);
|
|
73
|
+
}
|
|
74
|
+
else if (entry.isFile()) {
|
|
75
|
+
acc.push(abs);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
for (const entry of entries) {
|
|
81
|
+
if (entry.isDirectory()) {
|
|
82
|
+
await walkAndMatch(join(dir, entry.name), remaining, acc);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
await walkAndMatch(dir, rest, acc);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
for (const entry of entries) {
|
|
89
|
+
if (!matchSegment(head, entry.name)) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const abs = join(dir, entry.name);
|
|
93
|
+
if (rest.length === 0) {
|
|
94
|
+
if (entry.isFile()) {
|
|
95
|
+
acc.push(abs);
|
|
96
|
+
}
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (entry.isDirectory()) {
|
|
100
|
+
await walkAndMatch(abs, rest, acc);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function matchSegment(pattern, name) {
|
|
105
|
+
if (!hasGlob(pattern)) {
|
|
106
|
+
return pattern === name;
|
|
107
|
+
}
|
|
108
|
+
const re = new RegExp(`^${pattern
|
|
109
|
+
.split("")
|
|
110
|
+
.map((c) => {
|
|
111
|
+
if (c === "*") {
|
|
112
|
+
return "[^/]*";
|
|
113
|
+
}
|
|
114
|
+
if (c === "?") {
|
|
115
|
+
return "[^/]";
|
|
116
|
+
}
|
|
117
|
+
return c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
118
|
+
})
|
|
119
|
+
.join("")}$`);
|
|
120
|
+
return re.test(name);
|
|
121
|
+
}
|
|
122
|
+
function hasGlob(value) {
|
|
123
|
+
return /[*?[\]]/.test(value);
|
|
124
|
+
}
|
|
125
|
+
async function isFile(abs) {
|
|
126
|
+
try {
|
|
127
|
+
const s = await stat(abs);
|
|
128
|
+
return s.isFile();
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async function readConfig(path) {
|
|
135
|
+
try {
|
|
136
|
+
return await readFile(path, "utf8");
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
throw configFailure("config-missing", ".sdd/config.json is missing or unreadable", errorMessage(error), path);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function parseConfigJson(text, path) {
|
|
143
|
+
try {
|
|
144
|
+
return JSON.parse(text);
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
throw configFailure("config-invalid", ".sdd/config.json is not valid JSON", errorMessage(error), path);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function errorMessage(error) {
|
|
151
|
+
return error instanceof Error ? error.message : String(error);
|
|
152
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type RecordWriteResult } from "../domain/RecordWrite.js";
|
|
2
|
+
import type { RecordConfigPort } from "../ports/outbound/RecordConfigPort.js";
|
|
3
|
+
import type { RecordFileReader } from "../ports/outbound/RecordFileReader.js";
|
|
4
|
+
import type { RecordFileWriter } from "../ports/outbound/RecordFileWriter.js";
|
|
5
|
+
type WritePorts = {
|
|
6
|
+
config: RecordConfigPort;
|
|
7
|
+
files: RecordFileReader;
|
|
8
|
+
writer: RecordFileWriter;
|
|
9
|
+
};
|
|
10
|
+
export declare function addRecord(cwd: string, afterId: string, rawBody: string, ports: WritePorts): Promise<RecordWriteResult>;
|
|
11
|
+
export {};
|