ultra-builder-pro-cli 0.25.1 → 0.26.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/.ultra-template/changes/abandoned/.gitkeep +0 -0
- package/.ultra-template/contexts/TEMPLATE.md +94 -0
- package/.ultra-template/decisions/.gitkeep +1 -0
- package/.ultra-template/evidence/.gitkeep +1 -0
- package/.ultra-template/north-star.md +46 -0
- package/.ultra-template/research/.gitkeep +1 -0
- package/.ultra-template/specs/discovery.md +2 -2
- package/.ultra-template/tasks.json +3 -0
- package/.ultra-template/test-report.json +27 -0
- package/CHANGELOG.md +107 -1
- package/README.md +217 -579
- package/adapters/_shared/delegate-profiles/kimi-read-only.md +15 -0
- package/adapters/_shared/delegate-profiles/kimi-write.md +17 -0
- package/adapters/_shared/file-ops.cjs +76 -2
- package/adapters/_shared/host-profile.cjs +97 -0
- package/adapters/_shared/plugin-core.cjs +204 -0
- package/adapters/_shared/runtime-assets.cjs +31 -96
- package/adapters/claude.js +92 -291
- package/adapters/codex.js +309 -842
- package/adapters/grok.js +143 -1469
- package/adapters/kimi.js +124 -1053
- package/adapters/opencode.js +230 -742
- package/bin/delegate-worker.cjs +328 -0
- package/bin/delegate.cjs +463 -0
- package/bin/install.js +38 -11
- package/docs/ARCHITECTURE.md +125 -216
- package/docs/ARTIFACT-AUTHORITY.md +90 -268
- package/docs/DECISIONS.md +77 -87
- package/docs/PHILOSOPHY.md +208 -0
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +65 -39
- package/docs/RUNTIME-COMPAT-MATRIX.md +140 -155
- package/docs/SKILL-AUTHORING.md +94 -73
- package/docs/WORKFLOW-LIFECYCLE.md +97 -189
- package/hooks/README.md +19 -37
- package/hooks/_common.py +113 -0
- package/hooks/adapters/codex.py +9 -16
- package/hooks/adapters/grok.py +5 -5
- package/hooks/adapters/kimi.py +8 -14
- package/hooks/block_dangerous_commands.py +201 -0
- package/hooks/compact_context.py +65 -0
- package/hooks/mid_workflow_recall.py +25 -0
- package/hooks/post_edit_guard.py +105 -0
- package/hooks/session_context.py +30 -0
- package/package.json +17 -52
- package/skills/ultra-change/SKILL.md +115 -50
- package/skills/ultra-change/references/change-contract.md +68 -3
- package/skills/ultra-delegate/SKILL.md +89 -0
- package/skills/ultra-delegate/references/delegation-contract.md +59 -0
- package/skills/ultra-delegate/scripts/delegate_wait.py +86 -0
- package/skills/ultra-deliver/SKILL.md +98 -42
- package/skills/ultra-deliver/references/baseline-reconciliation.md +39 -20
- package/skills/ultra-dev/SKILL.md +115 -62
- package/skills/ultra-dev/references/debugging.md +22 -0
- package/skills/ultra-domain-modeling/SKILL.md +79 -0
- package/skills/ultra-grilling/SKILL.md +118 -0
- package/skills/ultra-grilling/references/reframing.md +72 -0
- package/skills/ultra-init/SKILL.md +102 -37
- package/skills/ultra-init/references/brownfield-adoption.md +33 -83
- package/skills/ultra-init/scripts/init_project.cjs +84 -0
- package/skills/ultra-plan/SKILL.md +101 -68
- package/skills/ultra-research/SKILL.md +68 -48
- package/skills/ultra-research/references/00-problem-validation.md +5 -5
- package/skills/ultra-research/references/01-opportunity-discovery.md +3 -3
- package/skills/ultra-research/references/02-market-assessment.md +5 -4
- package/skills/ultra-research/references/03-competitive-landscape.md +3 -3
- package/skills/ultra-research/references/04-product-strategy.md +3 -3
- package/skills/ultra-research/references/05-assumptions-validation.md +5 -5
- package/skills/ultra-research/references/10-user-personas.md +3 -2
- package/skills/ultra-research/references/11-user-scenarios.md +3 -3
- package/skills/ultra-research/references/20-user-stories.md +4 -3
- package/skills/ultra-research/references/21-features-scope.md +3 -2
- package/skills/ultra-research/references/22-success-metrics.md +3 -3
- package/skills/ultra-research/references/30-architecture-context.md +3 -3
- package/skills/ultra-research/references/31-solution-strategy.md +3 -3
- package/skills/ultra-research/references/32-building-blocks.md +3 -3
- package/skills/ultra-research/references/40-deployment.md +3 -3
- package/skills/ultra-research/references/41-quality-risks.md +3 -3
- package/skills/ultra-research/references/99-synthesis.md +7 -7
- package/skills/ultra-review/SKILL.md +84 -32
- package/skills/ultra-review/references/code.md +17 -0
- package/skills/ultra-review/references/comments.md +12 -0
- package/skills/ultra-review/references/design.md +12 -0
- package/skills/ultra-review/references/errors.md +13 -0
- package/skills/ultra-review/references/spec.md +13 -0
- package/skills/ultra-review/references/tests.md +12 -0
- package/skills/ultra-review/references/worker-packet.md +40 -0
- package/skills/ultra-review/scripts/review_wait.py +26 -15
- package/skills/ultra-status/SKILL.md +61 -36
- package/skills/ultra-tdd/SKILL.md +81 -0
- package/skills/ultra-tdd/references/templates/README.md +20 -0
- package/skills/ultra-tdd/references/templates/feature-flag-default-audit.sh +79 -0
- package/skills/ultra-tdd/references/templates/persistence-real.ts +78 -0
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.py +78 -0
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.ts +73 -0
- package/skills/ultra-tdd/references/templates/vertical-slice.ts +66 -0
- package/skills/ultra-tdd/references/test-execution.md +18 -0
- package/skills/ultra-test/SKILL.md +75 -30
- package/skills/ultra-test/references/export-syntax.md +18 -0
- package/skills/ultra-test/scripts/worktree_digest.cjs +54 -0
- package/skills/ultra-think/SKILL.md +49 -27
- package/skills/ultra-think/references/autonomy-boundary.md +48 -0
- package/skills/ultra-think/references/interaction-boundary.md +18 -33
- package/.ultra-template/docs/research/README.md +0 -55
- package/.ultra-template/reports/templates/delivery-report.json +0 -17
- package/.ultra-template/reports/templates/test-report.json +0 -18
- package/.ultra-template/tasks/tasks.json +0 -15
- package/.ultra-template/templates/task-context.md +0 -19
- package/adapters/_shared/codex-assets.cjs +0 -725
- package/adapters/_shared/codex-ultra-tools-entry.cjs +0 -11
- package/adapters/_shared/interaction-contract.cjs +0 -129
- package/adapters/_shared/path-rewrite.cjs +0 -79
- package/adapters/_shared/runtime-native.cjs +0 -132
- package/adapters/_shared/settings-merge.cjs +0 -76
- package/agents/code-reviewer.md +0 -42
- package/agents/debugger.md +0 -40
- package/agents/review-code.md +0 -49
- package/agents/review-comments.md +0 -38
- package/agents/review-coordinator.md +0 -56
- package/agents/review-design.md +0 -40
- package/agents/review-errors.md +0 -43
- package/agents/review-spec.md +0 -39
- package/agents/review-tests.md +0 -40
- package/agents/tdd-runner.md +0 -38
- package/bin/orchestrator.js +0 -383
- package/commands/ultra-change.md +0 -10
- package/commands/ultra-deliver.md +0 -10
- package/commands/ultra-dev.md +0 -10
- package/commands/ultra-doctor.md +0 -10
- package/commands/ultra-init.md +0 -10
- package/commands/ultra-plan.md +0 -10
- package/commands/ultra-research.md +0 -10
- package/commands/ultra-review.md +0 -10
- package/commands/ultra-status.md +0 -10
- package/commands/ultra-test.md +0 -10
- package/commands/ultra-think.md +0 -10
- package/docs/AGENT-CONTEXT.md +0 -191
- package/docs/COMMIT-HASH-BACKFILL.md +0 -51
- package/docs/LEGACY-CLI-CROSSWALK.md +0 -175
- package/docs/ROADMAP.md +0 -101
- package/docs/STATE-DB-ACCESS-POLICY.md +0 -161
- package/hooks/active_task_context.py +0 -152
- package/hooks/context_envelope.py +0 -100
- package/hooks/health_check.py +0 -196
- package/hooks/pre_stop_check.py +0 -66
- package/hooks/runtime_paths.py +0 -396
- package/hooks/subagent_tracker.py +0 -88
- package/hooks/workflow_checkpoint.py +0 -76
- package/hooks/workflow_context.py +0 -48
- package/hooks/workflow_resume.py +0 -84
- package/mcp-server/hook-context.cjs +0 -38
- package/mcp-server/hook-event.cjs +0 -34
- package/mcp-server/lib/archive-journal.cjs +0 -1119
- package/mcp-server/lib/archive-mutation-worker.py +0 -365
- package/mcp-server/lib/artifact-registry.cjs +0 -1232
- package/mcp-server/lib/baseline-workflow.cjs +0 -1193
- package/mcp-server/lib/canonical-json.cjs +0 -19
- package/mcp-server/lib/change-authority.cjs +0 -26
- package/mcp-server/lib/change-packet.cjs +0 -790
- package/mcp-server/lib/change-workflow.cjs +0 -65
- package/mcp-server/lib/context-envelope.cjs +0 -581
- package/mcp-server/lib/context-paths.cjs +0 -142
- package/mcp-server/lib/decision-records.cjs +0 -423
- package/mcp-server/lib/delivery-transaction.cjs +0 -471
- package/mcp-server/lib/doctor-backup-worker.cjs +0 -328
- package/mcp-server/lib/doctor.cjs +0 -627
- package/mcp-server/lib/git-bootstrap.cjs +0 -651
- package/mcp-server/lib/init-project.cjs +0 -1021
- package/mcp-server/lib/kernel-change-workflow.cjs +0 -2029
- package/mcp-server/lib/legacy-change-workflow.cjs +0 -3032
- package/mcp-server/lib/managed-file-write.cjs +0 -172
- package/mcp-server/lib/plan-checkpoint.cjs +0 -177
- package/mcp-server/lib/plan-publication-worker.cjs +0 -830
- package/mcp-server/lib/plan-store.cjs +0 -794
- package/mcp-server/lib/prd-parser.cjs +0 -83
- package/mcp-server/lib/pricing.cjs +0 -44
- package/mcp-server/lib/project-context.cjs +0 -178
- package/mcp-server/lib/projector.cjs +0 -386
- package/mcp-server/lib/provider-refs.cjs +0 -42
- package/mcp-server/lib/runtime-paths.cjs +0 -1714
- package/mcp-server/lib/runtime-state.cjs +0 -222
- package/mcp-server/lib/safe-project-file.cjs +0 -385
- package/mcp-server/lib/stage-checkpoints.cjs +0 -463
- package/mcp-server/lib/state-authority.cjs +0 -95
- package/mcp-server/lib/state-db.cjs +0 -1580
- package/mcp-server/lib/state-ops.cjs +0 -1428
- package/mcp-server/lib/task-contract.cjs +0 -404
- package/mcp-server/lib/task-expander.cjs +0 -105
- package/mcp-server/lib/task-ledger.cjs +0 -1964
- package/mcp-server/lib/telemetry.cjs +0 -85
- package/mcp-server/lib/topo.cjs +0 -131
- package/mcp-server/lib/ultra-facade.cjs +0 -2684
- package/mcp-server/lib/worker-packet.cjs +0 -358
- package/mcp-server/server.cjs +0 -437
- package/orchestrator/auto-merge.cjs +0 -128
- package/orchestrator/daemon.cjs +0 -264
- package/orchestrator/dispatch-rules.cjs +0 -78
- package/orchestrator/parallel-orchestrator.cjs +0 -903
- package/orchestrator/planner/plan-builder.cjs +0 -125
- package/orchestrator/recovery.cjs +0 -188
- package/orchestrator/session-close-journal-worker.cjs +0 -5
- package/orchestrator/session-close-journal.cjs +0 -1014
- package/orchestrator/session-runner.cjs +0 -1153
- package/orchestrator/worktree-manager.cjs +0 -282
- package/settings.json +0 -7
- package/skills/cc-collab/SKILL.md +0 -42
- package/skills/code-review-expert/SKILL.md +0 -43
- package/skills/code-review-expert/references/correctness-reliability.md +0 -30
- package/skills/code-review-expert/references/design-boundaries.md +0 -26
- package/skills/code-review-expert/references/removal-plan.md +0 -24
- package/skills/codex-collab/SKILL.md +0 -49
- package/skills/codex-collab/evals/evals.json +0 -35
- package/skills/integration-rules/SKILL.md +0 -32
- package/skills/security-rules/SKILL.md +0 -38
- package/skills/testing-rules/SKILL.md +0 -32
- package/skills/ultra-doctor/SKILL.md +0 -41
- package/skills/ultra-plan/references/semantic-preflight.md +0 -20
- package/skills/ultra-verify/SKILL.md +0 -46
- package/skills/ultra-verify/evals/evals.json +0 -46
- package/skills/ultra-verify/references/cross-verify-modes.md +0 -28
- package/skills/ultra-verify/references/evidence-status.md +0 -21
- package/skills/ultra-verify/scripts/verify_wait.py +0 -94
- package/spec/cli-protocol.md +0 -102
- package/spec/mcp-tools.yaml +0 -312
- package/spec/schemas/baseline-reconciliation.v1.schema.json +0 -73
- package/spec/schemas/change-delta.v1.schema.json +0 -153
- package/spec/schemas/command-manifest.schema.json +0 -29
- package/spec/schemas/context-file.v4.5.schema.json +0 -54
- package/spec/schemas/delivery-report.v1.schema.json +0 -66
- package/spec/schemas/documentation-reconciliation.v1.schema.json +0 -106
- package/spec/schemas/mcp-tools.schema.json +0 -136
- package/spec/schemas/skill-manifest.schema.json +0 -24
- package/spec/schemas/state-db.sql +0 -641
- package/spec/schemas/task-ledger.v2.schema.json +0 -296
- package/spec/schemas/tasks.v4.5.schema.json +0 -96
- package/spec/schemas/test-report.v1.schema.json +0 -225
- package/ultra-tools/cli.cjs +0 -129
- package/ultra-tools/commands/db.cjs +0 -226
- package/ultra-tools/commands/legacy-memory.cjs +0 -159
- package/ultra-tools/commands/migrate.cjs +0 -1386
- package/ultra-tools/commands/session.cjs +0 -213
- package/ultra-tools/commands/status.cjs +0 -559
- package/ultra-tools/commands/system.cjs +0 -498
- package/ultra-tools/commands/task.cjs +0 -105
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Validate and normalize a task graph derived from a PRD by the current host.
|
|
4
|
-
// The MCP runtime is deliberately deterministic: model judgment stays with
|
|
5
|
-
// the host that already owns the user interaction, while this module owns the
|
|
6
|
-
// machine contract before state.db is mutated.
|
|
7
|
-
|
|
8
|
-
const Ajv = require('ajv/dist/2020');
|
|
9
|
-
const TASK_LIST_SCHEMA = Object.freeze({
|
|
10
|
-
type: 'object',
|
|
11
|
-
required: ['tasks'],
|
|
12
|
-
properties: {
|
|
13
|
-
tasks: {
|
|
14
|
-
type: 'array',
|
|
15
|
-
minItems: 1,
|
|
16
|
-
items: {
|
|
17
|
-
type: 'object',
|
|
18
|
-
required: ['id', 'title', 'type', 'priority'],
|
|
19
|
-
properties: {
|
|
20
|
-
id: { type: 'string', pattern: '^[a-zA-Z0-9_\\-]+$' },
|
|
21
|
-
title: { type: 'string', minLength: 3, maxLength: 200 },
|
|
22
|
-
type: { type: 'string', minLength: 1, maxLength: 80 },
|
|
23
|
-
priority: { type: 'string', minLength: 1, maxLength: 80 },
|
|
24
|
-
complexity: { type: 'integer', minimum: 1, maximum: 10 },
|
|
25
|
-
deps: { type: 'array', items: { type: 'string' } },
|
|
26
|
-
files_modified: { type: 'array', items: { type: 'string' } },
|
|
27
|
-
},
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
34
|
-
const validateTaskList = ajv.compile(TASK_LIST_SCHEMA);
|
|
35
|
-
|
|
36
|
-
class PrdParseError extends Error {
|
|
37
|
-
constructor(code, message, cause) {
|
|
38
|
-
super(message);
|
|
39
|
-
this.name = 'PrdParseError';
|
|
40
|
-
this.code = code;
|
|
41
|
-
if (cause) this.cause = cause;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function parsePrd(tasks, { tag, changeId } = {}) {
|
|
46
|
-
if (!Array.isArray(tasks) || tasks.length === 0) {
|
|
47
|
-
throw new PrdParseError('NO_INPUT', 'at least one host-derived task is required');
|
|
48
|
-
}
|
|
49
|
-
const input = { tasks };
|
|
50
|
-
if (!validateTaskList(input)) {
|
|
51
|
-
throw new PrdParseError(
|
|
52
|
-
'PARSE_FAILED',
|
|
53
|
-
`task graph failed schema: ${ajv.errorsText(validateTaskList.errors)}`,
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const ids = new Set();
|
|
58
|
-
const normalized = tasks.map((t) => {
|
|
59
|
-
if (ids.has(t.id)) {
|
|
60
|
-
throw new PrdParseError('PARSE_FAILED', `duplicate task id "${t.id}" in task graph`);
|
|
61
|
-
}
|
|
62
|
-
ids.add(t.id);
|
|
63
|
-
return {
|
|
64
|
-
id: t.id,
|
|
65
|
-
title: t.title,
|
|
66
|
-
type: t.type,
|
|
67
|
-
priority: t.priority,
|
|
68
|
-
complexity: t.complexity ?? null,
|
|
69
|
-
deps: Array.isArray(t.deps) ? t.deps : [],
|
|
70
|
-
files_modified: Array.isArray(t.files_modified) ? t.files_modified : [],
|
|
71
|
-
tag: tag || null,
|
|
72
|
-
change_id: changeId || null,
|
|
73
|
-
};
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
return { tasks: normalized };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
module.exports = {
|
|
80
|
-
parsePrd,
|
|
81
|
-
PrdParseError,
|
|
82
|
-
TASK_LIST_SCHEMA,
|
|
83
|
-
};
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Phase 6.2 — Runtime × model pricing table (USD per token).
|
|
4
|
-
//
|
|
5
|
-
// Last updated: 2026-04-17. Public rate cards change — re-check before
|
|
6
|
-
// trusting the cost panel for billing. Numbers below are per token (not
|
|
7
|
-
// per million), so a 1000-token input at 3e-6 costs $0.003.
|
|
8
|
-
//
|
|
9
|
-
// Sources (2026-04):
|
|
10
|
-
// Claude: https://www.anthropic.com/pricing
|
|
11
|
-
// OpenAI: https://platform.openai.com/docs/pricing
|
|
12
|
-
//
|
|
13
|
-
// Pricing is exact-model only. A runtime label is not a billable model and
|
|
14
|
-
// host applications can route the same runtime through different providers.
|
|
15
|
-
// Missing or unknown models therefore return null instead of a fabricated
|
|
16
|
-
// estimate.
|
|
17
|
-
|
|
18
|
-
const PRICING = Object.freeze({
|
|
19
|
-
claude: Object.freeze({
|
|
20
|
-
'claude-opus-4-7': { input: 15e-6, output: 75e-6 },
|
|
21
|
-
'claude-sonnet-4-6': { input: 3e-6, output: 15e-6 },
|
|
22
|
-
'claude-haiku-4-5': { input: 0.8e-6, output: 4e-6 },
|
|
23
|
-
}),
|
|
24
|
-
codex: Object.freeze({
|
|
25
|
-
'gpt-5.4': { input: 2e-6, output: 10e-6 },
|
|
26
|
-
}),
|
|
27
|
-
opencode: Object.freeze({}),
|
|
28
|
-
kimi: Object.freeze({}),
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
function computeCost(runtime, model, tokens_input, tokens_output) {
|
|
32
|
-
const r = PRICING[runtime];
|
|
33
|
-
if (!r) return null;
|
|
34
|
-
const p = model ? r[model] : null;
|
|
35
|
-
if (!p) return null;
|
|
36
|
-
const ti = Number(tokens_input) || 0;
|
|
37
|
-
const to = Number(tokens_output) || 0;
|
|
38
|
-
return ti * p.input + to * p.output;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
module.exports = {
|
|
42
|
-
PRICING,
|
|
43
|
-
computeCost,
|
|
44
|
-
};
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const path = require('node:path');
|
|
5
|
-
|
|
6
|
-
const Database = require('better-sqlite3');
|
|
7
|
-
|
|
8
|
-
const contextEnvelope = require('./context-envelope.cjs');
|
|
9
|
-
const { EXPECTED_VERSION, REQUIRED_TABLES } = require('./state-db.cjs');
|
|
10
|
-
const runtimePaths = require('./runtime-paths.cjs');
|
|
11
|
-
|
|
12
|
-
function diagnosticEnvelope(rootDir, runtime, code, details = {}) {
|
|
13
|
-
return {
|
|
14
|
-
schema_version: '1.0',
|
|
15
|
-
digest: null,
|
|
16
|
-
detail: 'summary',
|
|
17
|
-
envelope: {
|
|
18
|
-
project: {
|
|
19
|
-
root: '.',
|
|
20
|
-
runtime,
|
|
21
|
-
health: 'needs_attention',
|
|
22
|
-
},
|
|
23
|
-
git: null,
|
|
24
|
-
baseline: null,
|
|
25
|
-
change: null,
|
|
26
|
-
task: null,
|
|
27
|
-
decisions: [],
|
|
28
|
-
checkpoints: [],
|
|
29
|
-
evidence: [],
|
|
30
|
-
execution: { stage: 'project', session: null },
|
|
31
|
-
team_checkpoint: null,
|
|
32
|
-
diagnostics: {
|
|
33
|
-
warnings: [],
|
|
34
|
-
needs_attention: [{
|
|
35
|
-
code,
|
|
36
|
-
severity: 'needs_attention',
|
|
37
|
-
...details,
|
|
38
|
-
}],
|
|
39
|
-
hard_conflicts: [],
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
bytes: 0,
|
|
43
|
-
root: path.resolve(rootDir),
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function tableNames(db) {
|
|
48
|
-
return new Set(db.prepare(
|
|
49
|
-
"SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'",
|
|
50
|
-
).all().map((row) => row.name));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function latestSchemaVersion(db) {
|
|
54
|
-
return db.prepare(
|
|
55
|
-
'SELECT version FROM schema_version ORDER BY applied_at DESC, rowid DESC LIMIT 1',
|
|
56
|
-
).get()?.version || null;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function readProjectContextEnvelope(rootDir, {
|
|
60
|
-
runtime = 'hook',
|
|
61
|
-
stage = 'project',
|
|
62
|
-
scope = {},
|
|
63
|
-
} = {}) {
|
|
64
|
-
const root = path.resolve(rootDir || process.cwd());
|
|
65
|
-
if (!fs.existsSync(path.join(root, '.ultra'))) return null;
|
|
66
|
-
const dbPath = runtimePaths.locateStateDb(root);
|
|
67
|
-
if (!fs.existsSync(dbPath)) {
|
|
68
|
-
return diagnosticEnvelope(root, runtime, 'STATE_DB_MISSING');
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
let db;
|
|
72
|
-
try {
|
|
73
|
-
db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
74
|
-
db.pragma('query_only = ON');
|
|
75
|
-
db.pragma('busy_timeout = 1000');
|
|
76
|
-
const tables = tableNames(db);
|
|
77
|
-
if (!tables.has('schema_version')) {
|
|
78
|
-
return diagnosticEnvelope(root, runtime, 'STATE_SCHEMA_MIGRATION_REQUIRED', {
|
|
79
|
-
version: null,
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
const version = latestSchemaVersion(db);
|
|
83
|
-
if (version !== EXPECTED_VERSION) {
|
|
84
|
-
return diagnosticEnvelope(root, runtime, 'STATE_SCHEMA_MIGRATION_REQUIRED', {
|
|
85
|
-
version,
|
|
86
|
-
expected: EXPECTED_VERSION,
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
const missing = REQUIRED_TABLES.filter((name) => !tables.has(name));
|
|
90
|
-
if (missing.length > 0) {
|
|
91
|
-
return diagnosticEnvelope(root, runtime, 'STATE_SCHEMA_INCOMPLETE', {
|
|
92
|
-
missing_tables: missing,
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
return {
|
|
96
|
-
...contextEnvelope.buildEnvelope(
|
|
97
|
-
db,
|
|
98
|
-
{ detail: 'summary', stage, scope },
|
|
99
|
-
{ rootDir: root, runtime },
|
|
100
|
-
),
|
|
101
|
-
root,
|
|
102
|
-
};
|
|
103
|
-
} catch (error) {
|
|
104
|
-
return diagnosticEnvelope(root, runtime, 'STATE_DB_UNREADABLE', {
|
|
105
|
-
cause: error.code || error.message,
|
|
106
|
-
});
|
|
107
|
-
} finally {
|
|
108
|
-
if (db) db.close();
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function compact(value, maximum = 240) {
|
|
113
|
-
const normalized = String(value || '').replace(/\s+/g, ' ').trim();
|
|
114
|
-
if (normalized.length <= maximum) return normalized;
|
|
115
|
-
return `${normalized.slice(0, maximum - 3).trimEnd()}...`;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function renderProjectContextEnvelope(rootDir, context) {
|
|
119
|
-
if (!context?.envelope) return '';
|
|
120
|
-
const root = path.resolve(rootDir || context.root || process.cwd());
|
|
121
|
-
const envelope = context.envelope;
|
|
122
|
-
const baseline = envelope.baseline || {};
|
|
123
|
-
const change = envelope.change || {};
|
|
124
|
-
const task = envelope.task || {};
|
|
125
|
-
const lines = [
|
|
126
|
-
'[Ultra Context Envelope]',
|
|
127
|
-
`Project: ${root}`,
|
|
128
|
-
`Stage: ${envelope.execution?.stage || 'project'}`,
|
|
129
|
-
`Baseline: ${baseline.id || 'none'} (${
|
|
130
|
-
baseline.mode ? `${baseline.mode}/` : ''
|
|
131
|
-
}${baseline.status || 'none'})`,
|
|
132
|
-
`Change: ${change.id || 'none'} (${change.status || 'none'})`,
|
|
133
|
-
`Task: ${task.id || 'none'} (${task.status || 'none'})`,
|
|
134
|
-
];
|
|
135
|
-
for (const decision of (envelope.decisions || []).slice(0, 5)) {
|
|
136
|
-
lines.push(
|
|
137
|
-
`Accepted decision: ${decision.id || 'unknown'} — ${
|
|
138
|
-
compact(decision.decision || decision.summary || decision.question || 'recorded')
|
|
139
|
-
}`,
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
for (const checkpoint of (envelope.checkpoints || []).slice(0, 5)) {
|
|
143
|
-
lines.push(
|
|
144
|
-
`Checkpoint: ${checkpoint.stage || 'unknown'} ${
|
|
145
|
-
checkpoint.status || 'recorded'
|
|
146
|
-
} (${checkpoint.id || 'unknown'})`,
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
const warnings = envelope.diagnostics?.warnings || [];
|
|
150
|
-
const attention = envelope.diagnostics?.needs_attention || [];
|
|
151
|
-
const conflicts = envelope.diagnostics?.hard_conflicts || [];
|
|
152
|
-
if (warnings.length > 0) {
|
|
153
|
-
lines.push(`Warnings: ${warnings.map((item) => item.code).join(', ')}`);
|
|
154
|
-
}
|
|
155
|
-
if (attention.length > 0) {
|
|
156
|
-
lines.push(`Needs attention: ${attention.map((item) => item.code).join(', ')}`);
|
|
157
|
-
}
|
|
158
|
-
if (conflicts.length > 0) {
|
|
159
|
-
lines.push(`Hard conflicts: ${conflicts.map((item) => item.code).join(', ')}`);
|
|
160
|
-
}
|
|
161
|
-
if (envelope.team_checkpoint) {
|
|
162
|
-
lines.push(
|
|
163
|
-
`Team checkpoint: ${envelope.team_checkpoint.status || 'unknown'} ${
|
|
164
|
-
envelope.team_checkpoint.state_digest || ''
|
|
165
|
-
}`.trim(),
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
lines.push(
|
|
169
|
-
'Authority: digest-bound .ultra documents plus checkout-local .ultra/.runtime/state.db.',
|
|
170
|
-
'Hooks are observational. Use ultra.context for the complete live envelope and public Ultra tools for changes.',
|
|
171
|
-
);
|
|
172
|
-
return lines.join('\n');
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
module.exports = {
|
|
176
|
-
readProjectContextEnvelope,
|
|
177
|
-
renderProjectContextEnvelope,
|
|
178
|
-
};
|
|
@@ -1,386 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Projector: regenerates the read-only file views from .ultra/.runtime/state.db.
|
|
4
|
-
//
|
|
5
|
-
// state.db owns lifecycle and artifact bindings (D32); registered digest-bound
|
|
6
|
-
// files own semantic/evidence bodies. Live tasks.json and contexts/task-*.md are
|
|
7
|
-
// checkout-local projections — humans may read them, never write them. The projector
|
|
8
|
-
// is trigger-based: state-ops calls projectAll() after each successful
|
|
9
|
-
// write transaction. Projection output passes the v4.5 schemas under
|
|
10
|
-
// spec/schemas/ — see mcp-server/tests/projector.test.cjs for the
|
|
11
|
-
// round-trip check.
|
|
12
|
-
|
|
13
|
-
const fs = require('node:fs');
|
|
14
|
-
const path = require('node:path');
|
|
15
|
-
const crypto = require('node:crypto');
|
|
16
|
-
|
|
17
|
-
const ops = require('./state-ops.cjs');
|
|
18
|
-
const artifacts = require('./artifact-registry.cjs');
|
|
19
|
-
const contextPaths = require('./context-paths.cjs');
|
|
20
|
-
|
|
21
|
-
const SCHEMA_VERSION = '4.5';
|
|
22
|
-
const SOURCE_TAG = '.ultra/.runtime/state.db';
|
|
23
|
-
|
|
24
|
-
// Frozen SELECTs — values bind through @placeholders.
|
|
25
|
-
const TASK_PROJECTION_COLUMNS = 'id, title, type, priority, complexity, estimated_days, status, deps, files_modified, session_id, stale, tag, trace_to, outcome, slice_kind, public_seam, verification_command, acceptance_json, context_refs_json, docs_impact_json, ownership_json, context_file, completion_commit, created_at, updated_at';
|
|
26
|
-
const LIST_TASKS_FOR_PROJECTION_SQL = `SELECT ${TASK_PROJECTION_COLUMNS} FROM tasks ORDER BY created_at ASC`;
|
|
27
|
-
const READ_TASK_FOR_PROJECTION_SQL = `SELECT ${TASK_PROJECTION_COLUMNS} FROM tasks WHERE id = @id`;
|
|
28
|
-
const CONTRACT_START = '<!-- ultra:task-contract:start -->';
|
|
29
|
-
const CONTRACT_END = '<!-- ultra:task-contract:end -->';
|
|
30
|
-
|
|
31
|
-
function rowToProjection(row) {
|
|
32
|
-
if (!row) return null;
|
|
33
|
-
const out = { ...row };
|
|
34
|
-
for (const k of ['deps', 'files_modified']) {
|
|
35
|
-
if (typeof out[k] === 'string') {
|
|
36
|
-
try { out[k] = JSON.parse(out[k]); } catch { out[k] = null; }
|
|
37
|
-
}
|
|
38
|
-
if (out[k] === null || out[k] === undefined) delete out[k];
|
|
39
|
-
}
|
|
40
|
-
const jsonFields = {
|
|
41
|
-
acceptance_json: ['acceptance', []],
|
|
42
|
-
context_refs_json: ['context_refs', []],
|
|
43
|
-
docs_impact_json: ['docs_impact', { status: 'unknown', files: [], rationale: null }],
|
|
44
|
-
ownership_json: ['ownership', {}],
|
|
45
|
-
};
|
|
46
|
-
for (const [column, [field, fallback]] of Object.entries(jsonFields)) {
|
|
47
|
-
try { out[field] = typeof out[column] === 'string' ? JSON.parse(out[column]) : fallback; }
|
|
48
|
-
catch { out[field] = fallback; }
|
|
49
|
-
delete out[column];
|
|
50
|
-
}
|
|
51
|
-
if (out.stale !== undefined && out.stale !== null) out.stale = Boolean(out.stale);
|
|
52
|
-
for (const k of Object.keys(out)) {
|
|
53
|
-
if (out[k] === null || out[k] === undefined) delete out[k];
|
|
54
|
-
}
|
|
55
|
-
return out;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function defaultPaths(rootDir) {
|
|
59
|
-
return {
|
|
60
|
-
tasksJson: path.join(rootDir, '.ultra', '.runtime', 'projections', 'tasks.json'),
|
|
61
|
-
contextsDir: path.join(rootDir, '.ultra', '.runtime', 'projections', 'contexts'),
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function writeAtomic(file, contents) {
|
|
66
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
67
|
-
const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
68
|
-
fs.writeFileSync(tmp, contents);
|
|
69
|
-
fs.renameSync(tmp, file);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function dbContentTimestamp(db) {
|
|
73
|
-
const row = db.prepare("SELECT MAX(latest) AS ts FROM (SELECT MAX(updated_at) AS latest FROM tasks UNION ALL SELECT MAX(ts) AS latest FROM events UNION ALL SELECT MAX(applied_at) AS latest FROM schema_version)").get();
|
|
74
|
-
return (row && row.ts) || new Date(0).toISOString();
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function projectTasks(db, { tasksJson } = {}, opts = {}) {
|
|
78
|
-
const rootDir = opts.rootDir;
|
|
79
|
-
const target = tasksJson || defaultPaths(rootDir || '.').tasksJson;
|
|
80
|
-
const rows = db.prepare(LIST_TASKS_FOR_PROJECTION_SQL).all();
|
|
81
|
-
// generated_at = max(content timestamps) so identical state → identical
|
|
82
|
-
// bytes → idempotent commits (PLAN Phase 2.6 / Phase 2.8 chore commit).
|
|
83
|
-
const payload = {
|
|
84
|
-
schema_version: SCHEMA_VERSION,
|
|
85
|
-
generated_at: dbContentTimestamp(db),
|
|
86
|
-
source: SOURCE_TAG,
|
|
87
|
-
tasks: rows.map(rowToProjection),
|
|
88
|
-
};
|
|
89
|
-
writeAtomic(target, JSON.stringify(payload, null, 2) + '\n');
|
|
90
|
-
return { path: target, count: rows.length };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function buildContextDoc(taskRow) {
|
|
94
|
-
const headerLines = [
|
|
95
|
-
'---',
|
|
96
|
-
`task_id: ${taskRow.id}`,
|
|
97
|
-
`title: ${escapeYaml(taskRow.title)}`,
|
|
98
|
-
`status: ${taskRow.status}`,
|
|
99
|
-
];
|
|
100
|
-
if (taskRow.priority) headerLines.push(`priority: ${taskRow.priority}`);
|
|
101
|
-
if (taskRow.type) headerLines.push(`type: ${taskRow.type}`);
|
|
102
|
-
if (taskRow.session_id) headerLines.push(`session_id: ${taskRow.session_id}`);
|
|
103
|
-
if (taskRow.completion_commit) headerLines.push(`completion_commit: ${taskRow.completion_commit}`);
|
|
104
|
-
if (taskRow.stale) headerLines.push('stale: true');
|
|
105
|
-
headerLines.push(`schema_version: ${SCHEMA_VERSION}`);
|
|
106
|
-
headerLines.push(`generated_by: ${contextPaths.GENERATED_BY}`);
|
|
107
|
-
headerLines.push(`generated_at: ${taskRow.updated_at || new Date().toISOString()}`);
|
|
108
|
-
headerLines.push('---', '');
|
|
109
|
-
|
|
110
|
-
const body = [];
|
|
111
|
-
if (taskRow.stale) {
|
|
112
|
-
body.push([
|
|
113
|
-
`> ⚠️ STALE since ${taskRow.updated_at || new Date().toISOString()}`,
|
|
114
|
-
'> Specification evidence may have changed. Recompile the task context before continuing.',
|
|
115
|
-
'',
|
|
116
|
-
].join('\n'));
|
|
117
|
-
}
|
|
118
|
-
body.push(buildTaskContract(taskRow));
|
|
119
|
-
return headerLines.join('\n') + body.join('');
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function buildTaskContract(task) {
|
|
123
|
-
const acceptance = Array.isArray(task.acceptance) ? task.acceptance : [];
|
|
124
|
-
const refs = Array.isArray(task.context_refs) ? task.context_refs : [];
|
|
125
|
-
const docs = task.docs_impact || { status: 'unknown', files: [], rationale: null };
|
|
126
|
-
const ownership = task.ownership || {};
|
|
127
|
-
const lines = [
|
|
128
|
-
CONTRACT_START,
|
|
129
|
-
'## Execution Contract (generated from state.db)',
|
|
130
|
-
'',
|
|
131
|
-
`- Outcome: ${task.outcome || 'unresolved'}`,
|
|
132
|
-
`- Slice: ${task.slice_kind || 'unresolved'}`,
|
|
133
|
-
`- Public seam: ${task.public_seam || 'unresolved'}`,
|
|
134
|
-
`- Verification: ${task.verification_command || 'unresolved'}`,
|
|
135
|
-
`- Trace: ${task.trace_to || 'unresolved'}`,
|
|
136
|
-
`- Owner: ${ownership.owner || 'unresolved'}`,
|
|
137
|
-
`- Documentation: ${docs.status || 'unknown'}${docs.rationale ? ` — ${docs.rationale}` : ''}`,
|
|
138
|
-
'',
|
|
139
|
-
'### Acceptance',
|
|
140
|
-
'',
|
|
141
|
-
...(acceptance.length > 0
|
|
142
|
-
? acceptance.map((item) => `- ${item.id}: ${item.criterion} — verify with \`${item.verification}\``)
|
|
143
|
-
: ['- unresolved']),
|
|
144
|
-
'',
|
|
145
|
-
'### Context References',
|
|
146
|
-
'',
|
|
147
|
-
...(refs.length > 0
|
|
148
|
-
? refs.map((item) => `- \`${item.ref}\` — ${item.reason}`)
|
|
149
|
-
: ['- unresolved']),
|
|
150
|
-
'',
|
|
151
|
-
CONTRACT_END,
|
|
152
|
-
'',
|
|
153
|
-
];
|
|
154
|
-
return lines.join('\n');
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function escapeYaml(str) {
|
|
158
|
-
if (str == null) return '';
|
|
159
|
-
if (/[:#\[\]\{\}&\*!|>'"%@`]/.test(str) || /^\s|\s$/.test(str)) {
|
|
160
|
-
return JSON.stringify(str);
|
|
161
|
-
}
|
|
162
|
-
return str;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function legacyContextParseError(message) {
|
|
166
|
-
const error = new Error(message);
|
|
167
|
-
error.code = 'LEGACY_CONTEXT_PARSE_ERROR';
|
|
168
|
-
return error;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function extractAuthoredContextBytes(contents) {
|
|
172
|
-
const original = Buffer.isBuffer(contents) ? contents : Buffer.from(contents);
|
|
173
|
-
let body = original.toString('utf8');
|
|
174
|
-
const frontmatter = /^(?:\uFEFF)?---(?:\r\n|\n)[\s\S]*?(?:\r\n|\n)---(?:(?:\r\n|\n)|$)/.exec(body);
|
|
175
|
-
if (frontmatter) body = body.slice(frontmatter[0].length);
|
|
176
|
-
const staleBanner = /^> ⚠️ STALE[^\r\n]*(?:(?:\r\n|\n)>[^\r\n]*)*(?:(?:\r\n|\n){1,2}|$)/.exec(body);
|
|
177
|
-
if (staleBanner) body = body.slice(staleBanner[0].length);
|
|
178
|
-
|
|
179
|
-
let cursor = 0;
|
|
180
|
-
while (true) {
|
|
181
|
-
const start = body.indexOf(CONTRACT_START, cursor);
|
|
182
|
-
if (start === -1) break;
|
|
183
|
-
const markerEnd = body.indexOf(CONTRACT_END, start + CONTRACT_START.length);
|
|
184
|
-
if (markerEnd === -1) {
|
|
185
|
-
throw legacyContextParseError(
|
|
186
|
-
'legacy task context has an unterminated generated execution contract',
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
let end = markerEnd + CONTRACT_END.length;
|
|
190
|
-
if (body.startsWith('\r\n', end)) end += 2;
|
|
191
|
-
else if (body.startsWith('\n', end)) end += 1;
|
|
192
|
-
const removeFrom = body.slice(0, start).trim() === '' ? 0 : start;
|
|
193
|
-
body = body.slice(0, removeFrom) + body.slice(end);
|
|
194
|
-
cursor = removeFrom;
|
|
195
|
-
}
|
|
196
|
-
return body.trim() === '' ? null : Buffer.from(body, 'utf8');
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function promoteAuthoredContext(db, taskId, target, rootDir) {
|
|
200
|
-
if (!fs.existsSync(target.file)) return null;
|
|
201
|
-
const stat = fs.lstatSync(target.file);
|
|
202
|
-
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
203
|
-
throw new contextPaths.ContextPathError(
|
|
204
|
-
`task context must be a regular file: ${target.relative}`,
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
const sourceBytes = fs.readFileSync(target.file);
|
|
208
|
-
const bytes = extractAuthoredContextBytes(sourceBytes);
|
|
209
|
-
if (!bytes) return null;
|
|
210
|
-
const digest = crypto.createHash('sha256').update(bytes).digest('hex');
|
|
211
|
-
const sourceDigest = crypto.createHash('sha256').update(sourceBytes).digest('hex');
|
|
212
|
-
const safeTaskId = String(taskId).replace(/[^a-zA-Z0-9_-]+/g, '-');
|
|
213
|
-
const relative = `.ultra/docs/migration/legacy-context/${safeTaskId}-${digest.slice(0, 12)}.md`;
|
|
214
|
-
const file = path.resolve(rootDir, ...relative.split('/'));
|
|
215
|
-
const existed = fs.existsSync(file);
|
|
216
|
-
if (existed) {
|
|
217
|
-
const existingDigest = crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
218
|
-
if (existingDigest !== digest) {
|
|
219
|
-
throw new Error(`legacy context promotion digest conflict: ${relative}`);
|
|
220
|
-
}
|
|
221
|
-
} else {
|
|
222
|
-
writeAtomic(file, bytes);
|
|
223
|
-
}
|
|
224
|
-
try {
|
|
225
|
-
const recorded = artifacts.recordArtifact(db, {
|
|
226
|
-
owner_type: 'task',
|
|
227
|
-
owner_id: taskId,
|
|
228
|
-
kind: 'legacy_context_findings',
|
|
229
|
-
path: relative,
|
|
230
|
-
content_digest: digest,
|
|
231
|
-
source_refs: [{
|
|
232
|
-
type: 'external',
|
|
233
|
-
id: `legacy-context:${target.relative}`,
|
|
234
|
-
relation: 'imported_from',
|
|
235
|
-
}],
|
|
236
|
-
consumer_refs: [{ type: 'task', id: taskId, relation: 'consumed_by' }],
|
|
237
|
-
provenance: {
|
|
238
|
-
writer: 'ultra-projector',
|
|
239
|
-
operation: 'legacy-context-promotion',
|
|
240
|
-
},
|
|
241
|
-
metadata: {
|
|
242
|
-
original_path: target.relative,
|
|
243
|
-
original_digest: sourceDigest,
|
|
244
|
-
authored_body_digest: digest,
|
|
245
|
-
},
|
|
246
|
-
}, { rootDir });
|
|
247
|
-
return { path: relative, digest, artifact_id: recorded.artifact.id };
|
|
248
|
-
} catch (error) {
|
|
249
|
-
if (!existed) fs.rmSync(file, { force: true });
|
|
250
|
-
throw error;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function projectContext(db, taskId, { contextsDir } = {}, opts = {}) {
|
|
255
|
-
const row = db.prepare(READ_TASK_FOR_PROJECTION_SQL).get({ id: taskId });
|
|
256
|
-
if (!row) return null;
|
|
257
|
-
const projection = rowToProjection(row);
|
|
258
|
-
const rootDir = path.resolve(opts.rootDir || '.');
|
|
259
|
-
if (contextsDir) {
|
|
260
|
-
const supplied = path.resolve(contextsDir);
|
|
261
|
-
const canonical = defaultPaths(rootDir).contextsDir;
|
|
262
|
-
if (supplied !== canonical) {
|
|
263
|
-
throw new contextPaths.ContextPathError(
|
|
264
|
-
`custom context projection roots are not allowed: ${contextsDir}`,
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
const target = contextPaths.resolveContextPath(rootDir, projection.context_file, { taskId });
|
|
269
|
-
const legacyValue = String(projection.context_file || '')
|
|
270
|
-
.replaceAll('\\', '/')
|
|
271
|
-
.replace(/^\.\//, '');
|
|
272
|
-
const legacyDefault = `${contextPaths.LEGACY_CONTEXT_ROOT_RELATIVE}/${
|
|
273
|
-
path.posix.basename(target.relative)
|
|
274
|
-
}`;
|
|
275
|
-
const legacyTarget = contextPaths.resolveLegacyContextPath(
|
|
276
|
-
rootDir,
|
|
277
|
-
legacyValue.startsWith(`${contextPaths.LEGACY_CONTEXT_ROOT_RELATIVE}/`)
|
|
278
|
-
? legacyValue
|
|
279
|
-
: legacyDefault,
|
|
280
|
-
{ taskId },
|
|
281
|
-
);
|
|
282
|
-
const legacyPromoted = promoteAuthoredContext(db, taskId, legacyTarget, rootDir);
|
|
283
|
-
if (fs.existsSync(legacyTarget.file)
|
|
284
|
-
&& contextPaths.isGeneratedContextContents(fs.readFileSync(legacyTarget.file, 'utf8'))) {
|
|
285
|
-
fs.rmSync(legacyTarget.file, { force: true });
|
|
286
|
-
} else if (legacyPromoted) {
|
|
287
|
-
fs.rmSync(legacyTarget.file, { force: true });
|
|
288
|
-
}
|
|
289
|
-
const promoted = promoteAuthoredContext(db, taskId, target, rootDir) || legacyPromoted;
|
|
290
|
-
const next = buildContextDoc(projection);
|
|
291
|
-
writeAtomic(target.file, next);
|
|
292
|
-
return { path: target.file, task_id: taskId, promoted };
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function listContextFiles(directory) {
|
|
296
|
-
if (!fs.existsSync(directory)) return [];
|
|
297
|
-
const output = [];
|
|
298
|
-
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
299
|
-
const file = path.join(directory, entry.name);
|
|
300
|
-
if (entry.isSymbolicLink()) continue;
|
|
301
|
-
if (entry.isDirectory()) output.push(...listContextFiles(file));
|
|
302
|
-
else if (entry.isFile()) output.push(file);
|
|
303
|
-
}
|
|
304
|
-
return output;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function pruneGeneratedGhostsIn(db, rootDir, contextsDir, expected) {
|
|
308
|
-
const pruned = [];
|
|
309
|
-
for (const file of listContextFiles(contextsDir)) {
|
|
310
|
-
const relative = path.relative(rootDir, file).split(path.sep).join('/');
|
|
311
|
-
if (expected.has(relative) || path.basename(file) === 'TEMPLATE.md') continue;
|
|
312
|
-
const contents = fs.readFileSync(file);
|
|
313
|
-
if (!contextPaths.isGeneratedContextContents(contents)) continue;
|
|
314
|
-
const staged = `${file}.prune-${process.pid}-${Date.now()}`;
|
|
315
|
-
fs.renameSync(file, staged);
|
|
316
|
-
try {
|
|
317
|
-
ops.appendEvent(db, {
|
|
318
|
-
type: 'projection_pruned',
|
|
319
|
-
payload: { path: relative, reason: 'task_missing' },
|
|
320
|
-
});
|
|
321
|
-
fs.rmSync(staged, { force: true });
|
|
322
|
-
pruned.push(relative);
|
|
323
|
-
} catch (error) {
|
|
324
|
-
fs.renameSync(staged, file);
|
|
325
|
-
throw error;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
return pruned;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function pruneGeneratedGhosts(db, rootDir, expected) {
|
|
332
|
-
const runtime = pruneGeneratedGhostsIn(
|
|
333
|
-
db,
|
|
334
|
-
rootDir,
|
|
335
|
-
defaultPaths(rootDir).contextsDir,
|
|
336
|
-
expected,
|
|
337
|
-
);
|
|
338
|
-
const legacy = pruneGeneratedGhostsIn(
|
|
339
|
-
db,
|
|
340
|
-
rootDir,
|
|
341
|
-
path.join(rootDir, ...contextPaths.LEGACY_CONTEXT_ROOT_RELATIVE.split('/')),
|
|
342
|
-
new Set(),
|
|
343
|
-
);
|
|
344
|
-
return [...runtime, ...legacy].sort();
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
function projectAll(db, { rootDir = '.', tasksJson, contextsDir } = {}) {
|
|
348
|
-
const resolvedRoot = path.resolve(rootDir);
|
|
349
|
-
const paths = defaultPaths(resolvedRoot);
|
|
350
|
-
const tasksTarget = tasksJson || paths.tasksJson;
|
|
351
|
-
const contextsTarget = contextsDir || paths.contextsDir;
|
|
352
|
-
if (path.resolve(contextsTarget) !== paths.contextsDir) {
|
|
353
|
-
throw new contextPaths.ContextPathError(
|
|
354
|
-
`custom context projection roots are not allowed: ${contextsTarget}`,
|
|
355
|
-
);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
const rows = db.prepare("SELECT id, context_file FROM tasks").all();
|
|
359
|
-
const expected = new Set(rows.map((row) => (
|
|
360
|
-
contextPaths.resolveContextPath(resolvedRoot, row.context_file, { taskId: row.id }).relative
|
|
361
|
-
)));
|
|
362
|
-
const tasksResult = projectTasks(db, { tasksJson: tasksTarget });
|
|
363
|
-
const contextResults = [];
|
|
364
|
-
for (const r of rows) {
|
|
365
|
-
contextResults.push(projectContext(
|
|
366
|
-
db, r.id, { contextsDir: contextsTarget }, { rootDir: resolvedRoot },
|
|
367
|
-
));
|
|
368
|
-
}
|
|
369
|
-
const pruned = pruneGeneratedGhosts(db, resolvedRoot, expected);
|
|
370
|
-
return {
|
|
371
|
-
tasks_json: tasksResult,
|
|
372
|
-
contexts: contextResults.filter(Boolean),
|
|
373
|
-
pruned,
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
module.exports = {
|
|
378
|
-
SCHEMA_VERSION,
|
|
379
|
-
defaultPaths,
|
|
380
|
-
projectTasks,
|
|
381
|
-
projectContext,
|
|
382
|
-
projectAll,
|
|
383
|
-
rowToProjection,
|
|
384
|
-
extractAuthoredContextBytes,
|
|
385
|
-
promoteAuthoredContext,
|
|
386
|
-
};
|