@limina-labs/momentum 0.30.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/adapters/antigravity/adapter.js +196 -0
- package/adapters/antigravity/hooks.json +39 -0
- package/adapters/antigravity/instructions/AGENTS.md +638 -0
- package/adapters/antigravity/instructions/header.md +3 -0
- package/adapters/antigravity/instructions/surfaces.md +153 -0
- package/adapters/antigravity/instructions/vars.json +1 -0
- package/adapters/antigravity/scripts/antigravity-hook-adapter.sh +147 -0
- package/adapters/antigravity/skills/momentum-orient/SKILL.md +29 -0
- package/adapters/antigravity/skills/momentum-reviewer-architecture/SKILL.md +39 -0
- package/adapters/antigravity/skills/momentum-reviewer-qa/SKILL.md +38 -0
- package/adapters/antigravity/skills/momentum-reviewer-security/SKILL.md +42 -0
- package/adapters/antigravity/skills/swarm-supervisor/SKILL.md +75 -0
- package/adapters/antigravity/workflows/continue.md +39 -0
- package/adapters/antigravity/workflows/dispatch.md +49 -0
- package/adapters/antigravity/workflows/handoff.md +38 -0
- package/adapters/antigravity/workflows/review-code.md +84 -0
- package/adapters/antigravity/workflows/scout.md +42 -0
- package/adapters/antigravity/workflows/swarm.md +203 -0
- package/adapters/claude-code/adapter.js +148 -0
- package/adapters/claude-code/commands/continue.md +82 -0
- package/adapters/claude-code/commands/dispatch.md +154 -0
- package/adapters/claude-code/commands/handoff.md +85 -0
- package/adapters/claude-code/commands/review-code.md +194 -0
- package/adapters/claude-code/commands/scout.md +119 -0
- package/adapters/claude-code/commands/swarm.md +242 -0
- package/adapters/claude-code/instructions/header.md +3 -0
- package/adapters/claude-code/instructions/vars.json +1 -0
- package/adapters/claude-code/settings.json +36 -0
- package/adapters/codex/adapter.js +225 -0
- package/adapters/codex/agents/momentum-reviewer-architecture.toml +57 -0
- package/adapters/codex/agents/momentum-reviewer-qa.toml +52 -0
- package/adapters/codex/agents/momentum-reviewer-security.toml +57 -0
- package/adapters/codex/agents/swarm-supervisor.toml +76 -0
- package/adapters/codex/commands/continue.md +40 -0
- package/adapters/codex/commands/dispatch.md +111 -0
- package/adapters/codex/commands/handoff.md +57 -0
- package/adapters/codex/commands/scout.md +90 -0
- package/adapters/codex/commands/swarm.md +334 -0
- package/adapters/codex/hooks.json +36 -0
- package/adapters/codex/instructions/AGENTS.md +691 -0
- package/adapters/codex/instructions/header.md +3 -0
- package/adapters/codex/instructions/surfaces.md +206 -0
- package/adapters/codex/instructions/vars.json +1 -0
- package/adapters/codex/skills/momentum-orient/SKILL.md +29 -0
- package/adapters/opencode/adapter.js +224 -0
- package/adapters/opencode/agents/momentum-reviewer-architecture.md +54 -0
- package/adapters/opencode/agents/momentum-reviewer-qa.md +48 -0
- package/adapters/opencode/agents/momentum-reviewer-security.md +48 -0
- package/adapters/opencode/agents/swarm-supervisor.md +51 -0
- package/adapters/opencode/commands/continue.md +40 -0
- package/adapters/opencode/commands/dispatch.md +111 -0
- package/adapters/opencode/commands/handoff.md +57 -0
- package/adapters/opencode/commands/review-code.md +194 -0
- package/adapters/opencode/commands/scout.md +90 -0
- package/adapters/opencode/commands/swarm.md +334 -0
- package/adapters/opencode/instructions/AGENTS.md +604 -0
- package/adapters/opencode/instructions/header.md +3 -0
- package/adapters/opencode/instructions/surfaces.md +119 -0
- package/adapters/opencode/instructions/vars.json +1 -0
- package/adapters/opencode/plugins/momentum.js +219 -0
- package/adapters/opencode/skills/momentum-lanes/SKILL.md +30 -0
- package/adapters/opencode/skills/momentum-orient/SKILL.md +29 -0
- package/adapters/opencode/skills/momentum-track/SKILL.md +28 -0
- package/adapters/opencode/skills/momentum-validate/SKILL.md +31 -0
- package/bin/antigravity.js +112 -0
- package/bin/ecosystem.js +1302 -0
- package/bin/lanes.js +300 -0
- package/bin/momentum.js +1857 -0
- package/bin/okf.js +72 -0
- package/bin/orchestration-commands.js +305 -0
- package/bin/state-commands.js +218 -0
- package/bin/swarm.js +1104 -0
- package/bin/waves.js +147 -0
- package/core/adapter-capabilities.md +112 -0
- package/core/adapter-parity-matrix.md +170 -0
- package/core/commands/brainstorm-idea.md +98 -0
- package/core/commands/brainstorm-phase.md +129 -0
- package/core/commands/complete-phase.md +130 -0
- package/core/commands/ecosystem.md +161 -0
- package/core/commands/hotfix.md +64 -0
- package/core/commands/initiative.md +119 -0
- package/core/commands/lanes.md +94 -0
- package/core/commands/log.md +45 -0
- package/core/commands/migrate.md +70 -0
- package/core/commands/review.md +31 -0
- package/core/commands/session.md +79 -0
- package/core/commands/start-phase.md +161 -0
- package/core/commands/start-project.md +142 -0
- package/core/commands/sync-docs.md +81 -0
- package/core/commands/systematic-debug.md +46 -0
- package/core/commands/track.md +54 -0
- package/core/commands/validate.md +96 -0
- package/core/ecosystem/layout.md +128 -0
- package/core/ecosystem/lib/index.js +245 -0
- package/core/ecosystem/lib/initiative.js +258 -0
- package/core/ecosystem/lib/pointer.js +198 -0
- package/core/ecosystem/lib/state.js +295 -0
- package/core/ecosystem/schema/ecosystem.schema.json +85 -0
- package/core/ecosystem/schema/initiative.schema.json +61 -0
- package/core/ecosystem/scripts/session-append.sh +167 -0
- package/core/ecosystem/templates/ecosystem-agents.md +79 -0
- package/core/ecosystem/templates/ecosystem-claude.md +79 -0
- package/core/ecosystem/templates/ecosystem-settings-claude.json +14 -0
- package/core/ecosystem/templates/initiative-template.md +50 -0
- package/core/engines/subagent-dispatch.md +80 -0
- package/core/git-hooks/commit-msg +14 -0
- package/core/git-hooks/contract.js +176 -0
- package/core/git-hooks/pre-push +15 -0
- package/core/git-hooks/run-check.js +160 -0
- package/core/instructions/legacy-project-md-hashes.json +12 -0
- package/core/instructions/navigation.md +14 -0
- package/core/instructions/rules-body.md +454 -0
- package/core/lanes/lib/board.js +161 -0
- package/core/lanes/lib/land.js +301 -0
- package/core/lanes/lib/signals.js +207 -0
- package/core/lanes/lib/state.js +325 -0
- package/core/lib/frontmatter.js +163 -0
- package/core/lib/okf-migrate.js +374 -0
- package/core/lib/okf-types.js +61 -0
- package/core/lifecycle-contract.md +59 -0
- package/core/orchestration/capability-routing.js +120 -0
- package/core/orchestration/continue.js +102 -0
- package/core/orchestration/dispatch.js +366 -0
- package/core/orchestration/events.js +197 -0
- package/core/orchestration/handoff.js +317 -0
- package/core/orchestration/index.js +50 -0
- package/core/orchestration/run-artifact.js +93 -0
- package/core/orchestration/scout.js +327 -0
- package/core/orchestration/session-log.js +123 -0
- package/core/orchestration/tracking.js +180 -0
- package/core/orchestration/types.js +131 -0
- package/core/scripts/brainstorm-gate.sh +157 -0
- package/core/scripts/check-history-reminder.sh +110 -0
- package/core/scripts/sessionstart-handoff.sh +180 -0
- package/core/specs-templates/CLAUDE.md +484 -0
- package/core/specs-templates/README.md +32 -0
- package/core/specs-templates/specs/README.md +30 -0
- package/core/specs-templates/specs/adhoc/README.md +31 -0
- package/core/specs-templates/specs/adhoc/_TEMPLATE.md +29 -0
- package/core/specs-templates/specs/architecture/ecosystem.md +214 -0
- package/core/specs-templates/specs/backlog/backlog.md +46 -0
- package/core/specs-templates/specs/backlog/details/.gitkeep +0 -0
- package/core/specs-templates/specs/changelog/.gitkeep +0 -0
- package/core/specs-templates/specs/decisions/0000-template.md +33 -0
- package/core/specs-templates/specs/decisions/README.md +29 -0
- package/core/specs-templates/specs/decisions/impact-map.md +14 -0
- package/core/specs-templates/specs/decisions/index.md +4 -0
- package/core/specs-templates/specs/index.md +16 -0
- package/core/specs-templates/specs/phases/README.md +41 -0
- package/core/specs-templates/specs/phases/index.md +6 -0
- package/core/specs-templates/specs/planning/roadmap.md +23 -0
- package/core/specs-templates/specs/status.md +70 -0
- package/core/specs-templates/specs/vision/principles.md +16 -0
- package/core/specs-templates/specs/vision/project-charter.md +35 -0
- package/core/specs-templates/specs/vision/success-criteria.md +19 -0
- package/core/swarm/absorb.js +294 -0
- package/core/swarm/conductor.js +499 -0
- package/core/swarm/focus.js +151 -0
- package/core/swarm/inbox.js +222 -0
- package/core/swarm/join.js +138 -0
- package/core/swarm/lib/board.js +178 -0
- package/core/swarm/lib/brief.js +157 -0
- package/core/swarm/lib/git-sha-cache.js +71 -0
- package/core/swarm/lib/incremental-log.js +117 -0
- package/core/swarm/lib/manifest.js +578 -0
- package/core/swarm/lib/pre-merge.js +112 -0
- package/core/swarm/lib/saga.js +146 -0
- package/core/swarm/lib/sessions.js +83 -0
- package/core/swarm/lib/tokens.js +255 -0
- package/core/swarm/lib/wave-ordering.js +55 -0
- package/core/swarm/schema/board.schema.json +54 -0
- package/core/swarm/schema/contract.schema.json +56 -0
- package/core/swarm/schema/dispatch-run.schema.json +70 -0
- package/core/swarm/schema/manifest.schema.json +234 -0
- package/core/swarm/schema/signal.schema.json +61 -0
- package/core/swarm/signals.js +296 -0
- package/core/swarm/supervise.md +81 -0
- package/core/waves/lib/plan-graph.js +175 -0
- package/core/waves/lib/waves.js +83 -0
- package/package.json +46 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Sync all relevant documents based on the active phase's history log.
|
|
2
|
+
Token-efficient: reads history + 2 tiny indexes first, then only targeted files.
|
|
3
|
+
|
|
4
|
+
> **Which phase is yours (Rule 15):** the phase bound to your branch — sync
|
|
5
|
+
> from YOUR lane's history log. `status.md` is the fallback and the
|
|
6
|
+
> cross-lane overview. Never sync another active lane's history from here.
|
|
7
|
+
|
|
8
|
+
## Steps
|
|
9
|
+
|
|
10
|
+
### Step 1: Load history and indexes (cheap reads)
|
|
11
|
+
- Resolve your phase from the current branch: a `phase-*` branch with a
|
|
12
|
+
matching `specs/phases/<branch>/` directory binds to that phase; fallback =
|
|
13
|
+
the `specs/status.md` Active Phase table
|
|
14
|
+
- Read the history log → extract all entries:
|
|
15
|
+
- **Phase lane**: `specs/phases/<phase-bound-to-your-branch>/history.md`
|
|
16
|
+
- **No active phase** (ad-hoc work just shipped): the relevant
|
|
17
|
+
`specs/adhoc/<id>/record.md` (and `specs/adhoc/history.md` if present).
|
|
18
|
+
If there is no phase and no ad-hoc record, there is nothing to sync —
|
|
19
|
+
report "nothing to sync" and stop.
|
|
20
|
+
- Read the `tags:` frontmatter of each `specs/phases/*/overview.md` (OKF bundle)
|
|
21
|
+
- Read `specs/decisions/impact-map.md` (~3KB)
|
|
22
|
+
|
|
23
|
+
### Step 2: Build targeted file list
|
|
24
|
+
For each history entry:
|
|
25
|
+
- Extract Topics list
|
|
26
|
+
- Match each topic against phase `tags:` frontmatter → collect affected phase tasks.md files
|
|
27
|
+
- Look up each topic in the impact-map.md table → collect affected spec files/sections
|
|
28
|
+
- Deduplicate the combined list
|
|
29
|
+
- For monorepo: EXCLUDE files in `specs/architecture/`
|
|
30
|
+
(constitution — never auto-synced, only amended via formal process)
|
|
31
|
+
- **Multi-repo: PARTITION OUT cross-repo entries.** If a history entry has an
|
|
32
|
+
`Affects-specs:` path starting with `../` (or otherwise outside this repo),
|
|
33
|
+
add it to a separate "cross-repo" list. NEVER edit those files — you only
|
|
34
|
+
own this repo's docs.
|
|
35
|
+
|
|
36
|
+
### Step 3: Show sync plan (ALWAYS show before touching anything)
|
|
37
|
+
Present to user:
|
|
38
|
+
"Based on phase history, I will check and potentially update:
|
|
39
|
+
- [list of files]
|
|
40
|
+
Proceed? (yes/no)"
|
|
41
|
+
|
|
42
|
+
**If any cross-repo entries were partitioned out in Step 2:** also show them
|
|
43
|
+
under a "Cross-repo impact (NOT touching — sync the other repo manually)"
|
|
44
|
+
heading, listing each `Affects-specs: ../...` path. Tell the user which repo
|
|
45
|
+
they need to edit and which entries pointed there. Do NOT prompt for approval
|
|
46
|
+
on cross-repo paths — they're informational only.
|
|
47
|
+
|
|
48
|
+
If user says no → stop.
|
|
49
|
+
|
|
50
|
+
### Step 4: Read and assess (targeted reads only)
|
|
51
|
+
For each file in the list:
|
|
52
|
+
- Read only the relevant section
|
|
53
|
+
- Assess: does it need updating based on history entries?
|
|
54
|
+
|
|
55
|
+
### Step 5: Make updates (one at a time, fully visible)
|
|
56
|
+
For each file that needs updating:
|
|
57
|
+
- Show the user what will change
|
|
58
|
+
- Use the Edit tool to make the change
|
|
59
|
+
|
|
60
|
+
### Step 6: Update phase metadata if scope changed
|
|
61
|
+
If any [SCOPE_CHANGE] entries exist:
|
|
62
|
+
- Update the active phase's `overview.md` frontmatter `tags:` list
|
|
63
|
+
- Run `momentum okf index` to refresh the bundle listings
|
|
64
|
+
|
|
65
|
+
### Step 7: Commit all changes
|
|
66
|
+
```bash
|
|
67
|
+
git add <all modified spec files>
|
|
68
|
+
git commit -m "docs(phase-N): sync specs from phase history"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Step 8: Confirm completion
|
|
72
|
+
"Spec sync complete. Updated N files:
|
|
73
|
+
- [list]
|
|
74
|
+
Ready to run /complete-phase."
|
|
75
|
+
|
|
76
|
+
## Safeguards
|
|
77
|
+
- NEVER update files not in the targeted list
|
|
78
|
+
- NEVER update `specs/architecture/` (monorepo only — constitution is read-only)
|
|
79
|
+
- NEVER update files in another repo (paths starting with `../`) — you only own this repo's docs. Flag cross-repo entries to the user instead.
|
|
80
|
+
- ALWAYS show the plan (Step 3) before making any edits
|
|
81
|
+
- History entries are NEVER modified — only read
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Systematically isolate, reproduce, and resolve task execution failures.
|
|
2
|
+
|
|
3
|
+
This recipe enforces the **3-strikes systematic debugging/retry budget** defined under the Autonomous Execution Contract. If validation fails for a task, execute the strikes sequentially:
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Strike 1 — Analyze and Adjust
|
|
8
|
+
|
|
9
|
+
1. **Analyze validation output**: Look closely at the stdout/stderr, stack trace, and exit code.
|
|
10
|
+
2. **Hypothesize the cause**: Identify if the failure is due to a simple syntax error, a missing import, incorrect pathing, or a basic logical gap.
|
|
11
|
+
3. **Adjust**: Implement the simple correction directly.
|
|
12
|
+
4. **Re-run validation**: Execute the group's verification command.
|
|
13
|
+
* *Passes* → Check task `[x]` and proceed.
|
|
14
|
+
* *Fails* → Proceed to **Strike 2**.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Strike 2 — Reproduce and Isolate
|
|
19
|
+
|
|
20
|
+
1. **Write reproduction test**: Create a minimal unit test or a temporary scratch script that isolates the failing code path and reproduces the exact failure.
|
|
21
|
+
2. **Run isolation**: Run the reproduction case to confirm it fails in isolation.
|
|
22
|
+
3. **Implement isolation fix**: Fix the code under the reproduced test case.
|
|
23
|
+
4. **Verify isolation**: Confirm the reproduction case now passes.
|
|
24
|
+
5. **Re-run validation**: Execute the main group verification command.
|
|
25
|
+
* *Passes* → Check task `[x]` and proceed. Clean up any temporary scratch scripts.
|
|
26
|
+
* *Fails* → Proceed to **Strike 3**.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Strike 3 — Re-Read and Reframe
|
|
31
|
+
|
|
32
|
+
1. **Re-read specs**: Completely stop coding. Re-read:
|
|
33
|
+
* `specs/status.md` (to check active context)
|
|
34
|
+
* `specs/phases/phase-N-*/overview.md` (to re-read goals and deliverables)
|
|
35
|
+
* `specs/phases/phase-N-*/plan.md` (to review architectural choices)
|
|
36
|
+
2. **Identify gaps**: Figure out if the implementation approach is fundamentally incompatible with a spec constraint.
|
|
37
|
+
3. **Reframe**: Redesign the approach to align with specs.
|
|
38
|
+
4. **Draft proposed change**: Discuss the re-framed approach with the user in conversation. Do not write changes to specs.
|
|
39
|
+
5. **Obtain Approval**: Wait for user review before implementing the reframed code.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## See Also
|
|
44
|
+
|
|
45
|
+
* **Rule 12 (Verify Before Claim)**: Always run the actual verification command and capture output.
|
|
46
|
+
* **start-phase**: Automates end-to-end execution utilizing this debugging recipe on task failures.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Track a backlog item — bug, feature, tech debt, or enhancement.
|
|
2
|
+
|
|
3
|
+
## Steps
|
|
4
|
+
|
|
5
|
+
1. Read `specs/backlog/backlog.md` to find next available ID
|
|
6
|
+
|
|
7
|
+
2. Determine item type:
|
|
8
|
+
- Bug → `BUG-NNN`
|
|
9
|
+
- Feature → `FEAT-NNN`
|
|
10
|
+
- Tech Debt → `TD-NNN`
|
|
11
|
+
- Enhancement → `ENH-NNN`
|
|
12
|
+
|
|
13
|
+
3. Add row to appropriate table in `specs/backlog/backlog.md`:
|
|
14
|
+
- Priority: infer from urgency (default P2)
|
|
15
|
+
- Status: `open`
|
|
16
|
+
- Phase: target phase or `unscheduled`
|
|
17
|
+
- Detail: `—` for one-liners, `[→](details/{ID}.md)` if a detail file is created
|
|
18
|
+
|
|
19
|
+
4. Decide: one-liner or detail file?
|
|
20
|
+
|
|
21
|
+
**One-liner (no detail file needed):**
|
|
22
|
+
- Clear, self-contained issue (< 2 sentences to describe)
|
|
23
|
+
- No design choices to make
|
|
24
|
+
- Single file or single function affected
|
|
25
|
+
- Fix is obvious from the description
|
|
26
|
+
|
|
27
|
+
**Detail file required (`specs/backlog/details/{ID}.md`):**
|
|
28
|
+
- Requires design or multiple options to evaluate
|
|
29
|
+
- Touches multiple files, commands, or systems
|
|
30
|
+
- Has a non-obvious implementation path
|
|
31
|
+
- Cross-cutting impact (affects other phases or specs)
|
|
32
|
+
- Estimated > 30 min of work
|
|
33
|
+
|
|
34
|
+
If a detail file is needed, create it now with: context, options considered, open questions.
|
|
35
|
+
|
|
36
|
+
5. If P0, also update Critical Items table in `specs/status.md`
|
|
37
|
+
|
|
38
|
+
6. Commit:
|
|
39
|
+
```bash
|
|
40
|
+
git add specs/backlog/
|
|
41
|
+
git commit -m "docs(backlog): add {ID} - {short title}"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Cross-repo backlog items
|
|
45
|
+
|
|
46
|
+
`/track` operates on the current repo's `specs/backlog/`. The ecosystem
|
|
47
|
+
layer (Phase 9) does NOT aggregate backlogs across member repos in
|
|
48
|
+
Tier 1 — each repo's backlog stays authoritative for its own work. If
|
|
49
|
+
an item's scope genuinely spans repos, file it in the repo that owns
|
|
50
|
+
the most surface area and add a one-line cross-link from the others.
|
|
51
|
+
|
|
52
|
+
For cross-repo features as a whole (vs. individual backlog items), use
|
|
53
|
+
`/initiative create <slug>` — see the `/ecosystem` and `/initiative`
|
|
54
|
+
command recipes.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Check the spec structure health of the current momentum project.
|
|
2
|
+
|
|
3
|
+
Run with no arguments for a fast index-first check, or pass `--deep` for a full scan.
|
|
4
|
+
|
|
5
|
+
> **Which phase is yours (Rule 15):** the phase bound to your branch;
|
|
6
|
+
> `status.md` is the fallback and the cross-lane overview. Validation checks
|
|
7
|
+
> the whole board, not just your lane.
|
|
8
|
+
|
|
9
|
+
## Default Mode (index-first)
|
|
10
|
+
|
|
11
|
+
1. Read `specs/status.md` — verify required fields are present:
|
|
12
|
+
- `Last Updated`, `Current Phase`, `Latest Release`, `Health`
|
|
13
|
+
- Active Phase table exists. **"No active phase" is a VALID state** — a
|
|
14
|
+
single row reading `_(none)_` / `no active phase` (e.g. between phases, or
|
|
15
|
+
during ad-hoc `/hotfix` work) is NOT a failure. **Multiple rows are also
|
|
16
|
+
valid** — one row per active lane (Rule 15, concurrent workstreams). Only
|
|
17
|
+
flag a structurally missing/empty table.
|
|
18
|
+
- Report any missing fields as failures
|
|
19
|
+
|
|
20
|
+
2. Read `specs/backlog/backlog.md` — verify all 4 section tables present:
|
|
21
|
+
- Bugs, Features, Tech Debt, Enhancements
|
|
22
|
+
- Report any missing table as a failure
|
|
23
|
+
|
|
24
|
+
3. For each phase directory under `specs/phases/` (OKF bundle, ADR-0005):
|
|
25
|
+
- Verify all 4 files present: `overview.md`, `plan.md`, `tasks.md`, `history.md`
|
|
26
|
+
- Verify `overview.md` frontmatter carries `type: Phase` and a `status:`
|
|
27
|
+
(legacy projects may still have `specs/phases/index.json` instead —
|
|
28
|
+
recommend `momentum upgrade` to migrate)
|
|
29
|
+
- Report missing files or missing status frontmatter as failures
|
|
30
|
+
|
|
31
|
+
4. Cross-check active phase consistency:
|
|
32
|
+
- If an active phase is named, its directory must exist and its
|
|
33
|
+
`overview.md` frontmatter must say `status: in-progress` — report
|
|
34
|
+
mismatch as a failure. With multiple active lanes (Rule 15), check EVERY
|
|
35
|
+
row: each row's branch must bind to an existing phase directory.
|
|
36
|
+
- If `Current Phase` is "none" / no active phase, **skip this check** (valid
|
|
37
|
+
between-phases state).
|
|
38
|
+
|
|
39
|
+
4b. OKF conformance (delegates to the CLI):
|
|
40
|
+
- Run `momentum okf check` — report its violations as failures verbatim
|
|
41
|
+
- Run `momentum okf index` if listings are stale (safe — regenerates only
|
|
42
|
+
`specs/index.md`, `specs/phases/index.md`, `specs/decisions/index.md`)
|
|
43
|
+
|
|
44
|
+
5. Check `.claude/commands/` for standard momentum commands:
|
|
45
|
+
- Required: `brainstorm-idea`, `brainstorm-phase`, `start-project`, `start-phase`,
|
|
46
|
+
`complete-phase`, `log`, `sync-docs`, `track`, `migrate`, `validate`, `hotfix`
|
|
47
|
+
- Report any missing commands as warnings (not failures — project may predate them)
|
|
48
|
+
|
|
49
|
+
6. Report results:
|
|
50
|
+
```
|
|
51
|
+
✓ N checks passed
|
|
52
|
+
✗ N issues found:
|
|
53
|
+
- specs/phases/phase-3-gap-fixes/overview.md: missing `status:` frontmatter
|
|
54
|
+
- specs/status.md: missing field "Latest Release"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## `--deep` Flag (full scan)
|
|
58
|
+
|
|
59
|
+
Run all default mode checks, then additionally:
|
|
60
|
+
|
|
61
|
+
7. Walk ALL directories under `specs/phases/` — flag any directory whose
|
|
62
|
+
`overview.md` lacks `status:` frontmatter as an untracked phase
|
|
63
|
+
|
|
64
|
+
8. For each phase directory, read `tasks.md`:
|
|
65
|
+
- Extract all backlog ID references: `BUG-NNN`, `FEAT-NNN`, `TD-NNN`, `ENH-NNN`
|
|
66
|
+
- Verify each ID exists as a row in `specs/backlog/backlog.md`
|
|
67
|
+
- Report unresolved IDs as failures
|
|
68
|
+
|
|
69
|
+
9. For each phase directory, read `history.md`:
|
|
70
|
+
- Verify each entry has all required fields: type tag `[TYPE]`, date in `YYYY-MM-DD`
|
|
71
|
+
format, `Topics:`, `Affects-phases:`, `Affects-specs:`, `Detail:`
|
|
72
|
+
- Report malformed entries (missing fields) as warnings
|
|
73
|
+
|
|
74
|
+
10. Check `specs/changelog/` — for each phase whose `overview.md` frontmatter
|
|
75
|
+
says `status: complete`, verify at least one changelog file exists under
|
|
76
|
+
`specs/changelog/`
|
|
77
|
+
- Report completely absent changelog as a warning
|
|
78
|
+
|
|
79
|
+
11. Swarm member integrity (Phase 17+, only when ecosystem context exists):
|
|
80
|
+
- For each `<ecosystem-root>/swarms/<id>/manifest.json`:
|
|
81
|
+
- Validate against `core/swarm/schema/manifest.schema.json` (load + structural check)
|
|
82
|
+
- Every `manifest.repos.<id>` must reference a real `ecosystem.json` member
|
|
83
|
+
- Each repo's `phase_slug` must point at an existing phase directory
|
|
84
|
+
in that member repo
|
|
85
|
+
- For every phase brief that carries swarm frontmatter (parsed via
|
|
86
|
+
`core/swarm/lib/brief.js`):
|
|
87
|
+
- `swarm:` must resolve to a real swarm manifest at the ecosystem root
|
|
88
|
+
- `wave:` must match the wave that swarm has assigned this repo
|
|
89
|
+
- `initiative:` must match the swarm manifest's `initiative` field
|
|
90
|
+
- Report mismatch as a failure
|
|
91
|
+
- For every swarm with `status: complete`, verify the initiative's
|
|
92
|
+
`Per-repo contributions` section lists all swarm members.
|
|
93
|
+
- Report unresolved swarm references as failures; report orphaned
|
|
94
|
+
brief frontmatter (swarm: refers to non-existent swarm) as failures.
|
|
95
|
+
|
|
96
|
+
12. Append deep-scan results to the report before printing.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Ecosystem Root: On-Disk Layout
|
|
2
|
+
|
|
3
|
+
> A momentum **ecosystem** is a sibling git repo that coordinates a set
|
|
4
|
+
> of related momentum-installed repos as one product. Reading this
|
|
5
|
+
> file tells you everything about what lives where.
|
|
6
|
+
|
|
7
|
+
## Layout
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
<ecosystem-root>/
|
|
11
|
+
├── ecosystem.json ← manifest (members, roles, dep edges)
|
|
12
|
+
├── README.md ← human-readable description + worked example
|
|
13
|
+
├── initiatives/ ← cross-repo feature files
|
|
14
|
+
│ ├── README.md
|
|
15
|
+
│ ├── 0001-<slug>.md
|
|
16
|
+
│ └── 0002-<slug>.md
|
|
17
|
+
├── sessions/ ← daily activity logs
|
|
18
|
+
│ ├── .gitkeep
|
|
19
|
+
│ ├── 2026-06-07.md
|
|
20
|
+
│ └── 2026-06-08.md
|
|
21
|
+
├── .state/ ← runtime state (gitignored)
|
|
22
|
+
│ ├── active-initiative ← contents = slug or empty
|
|
23
|
+
│ └── last-session ← contents = ISO date
|
|
24
|
+
└── .gitignore ← `.state/` plus standard noise
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## File contracts
|
|
28
|
+
|
|
29
|
+
### `ecosystem.json`
|
|
30
|
+
Single source of truth for the ecosystem's membership and inter-repo
|
|
31
|
+
edges. Schema: `core/ecosystem/schema/ecosystem.schema.json`. Validated
|
|
32
|
+
on read.
|
|
33
|
+
|
|
34
|
+
### `initiatives/NNNN-<slug>.md`
|
|
35
|
+
Cross-repo feature record. Top YAML frontmatter block conforms to
|
|
36
|
+
`core/ecosystem/schema/initiative.schema.json`. Body is markdown
|
|
37
|
+
sections from the initiative template
|
|
38
|
+
(`core/ecosystem/templates/initiative-template.md`):
|
|
39
|
+
- **Why** — one paragraph
|
|
40
|
+
- **Per-repo contributions** — per-member bullets
|
|
41
|
+
- **Linked decisions** — ADR pointers (cross-repo OK)
|
|
42
|
+
- **Deploy chronology** — timestamps, repos, SHAs
|
|
43
|
+
- **Close** — populated by `momentum ecosystem initiative close`
|
|
44
|
+
|
|
45
|
+
Numbering is monotonically increasing across the ecosystem. The
|
|
46
|
+
filename slug is for human readability; the `id` integer is canonical.
|
|
47
|
+
|
|
48
|
+
### `sessions/YYYY-MM-DD.md`
|
|
49
|
+
One file per UTC date. Appended to atomically by the PostToolUse hook
|
|
50
|
+
in each member repo. Header line (first write of the day) names the
|
|
51
|
+
active initiative if any. Subsequent lines have the format:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
HH:MMZ [<member-id>] <event-summary> (<context>)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Examples:
|
|
58
|
+
```
|
|
59
|
+
03:24Z [sapience] commit 8a086117 — fix(memory): caller-accessible fanout
|
|
60
|
+
04:30Z [sapience] deploy success — image_tag=8a086117
|
|
61
|
+
09:50Z [sapience] PR #42 merged — list[T] query binding fix
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### `.state/active-initiative`
|
|
65
|
+
A single line, either empty or containing the slug of the currently
|
|
66
|
+
active initiative. Set by `momentum ecosystem initiative create`;
|
|
67
|
+
cleared by `… close`. The session-log hook reads this to inject the
|
|
68
|
+
"Active initiative: …" header line.
|
|
69
|
+
|
|
70
|
+
### `.state/last-session`
|
|
71
|
+
The ISO date of the most recent session file. Cheap cache so the hook
|
|
72
|
+
doesn't re-stat every directory entry.
|
|
73
|
+
|
|
74
|
+
## Discovery & limits
|
|
75
|
+
|
|
76
|
+
The ecosystem root is **discovered** by walking up from `$PWD` (max 5
|
|
77
|
+
parents by default) until a directory containing `ecosystem.json` is
|
|
78
|
+
found. The resolution is memoized per session.
|
|
79
|
+
|
|
80
|
+
**Configurable depth (ENH-022).** Override the parent-walk limit via
|
|
81
|
+
the `MOMENTUM_MAX_PARENT_WALK` environment variable. Honored in both
|
|
82
|
+
the JS path (`core/ecosystem/lib/index.js`, `core/ecosystem/lib/state.js`)
|
|
83
|
+
and the shell path (`core/ecosystem/scripts/session-append.sh`).
|
|
84
|
+
Non-numeric or negative values fall back to the default of 5.
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
MOMENTUM_MAX_PARENT_WALK=10 momentum ecosystem status
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Location-agnostic CLI (ENH-021).** `momentum ecosystem add`,
|
|
91
|
+
`remove`, and `status` resolve the ecosystem root from CWD by default
|
|
92
|
+
(via the walk-up above). Use `--ecosystem <path>` to override
|
|
93
|
+
explicitly:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
momentum ecosystem add ./client --ecosystem ../my-eco
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Concurrency (BUG-004).** Session-log writes are serialized via a
|
|
100
|
+
`mkdir`-based lock per session file (atomic on POSIX filesystems,
|
|
101
|
+
portable across macOS and Linux without depending on `flock`). Lock
|
|
102
|
+
acquisition has a ~5s budget; on timeout the event is dropped
|
|
103
|
+
silently rather than risking file corruption.
|
|
104
|
+
|
|
105
|
+
## Conventions
|
|
106
|
+
|
|
107
|
+
- The ecosystem layer **never writes** into a member repo's `specs/`.
|
|
108
|
+
It only writes to its own directories above. The single touch on a
|
|
109
|
+
member is one fenced line in its `CLAUDE.md` / `AGENTS.md`:
|
|
110
|
+
```html
|
|
111
|
+
<!-- ecosystem:begin -->
|
|
112
|
+
> Member of `<name>` ecosystem at `../<name>`. See ecosystem.json.
|
|
113
|
+
<!-- ecosystem:end -->
|
|
114
|
+
```
|
|
115
|
+
- The ecosystem root is a normal git repo. `momentum ecosystem init`
|
|
116
|
+
runs `git init` and produces the initial commit; subsequent changes
|
|
117
|
+
are user-managed (the ecosystem doesn't enforce a workflow on itself
|
|
118
|
+
beyond the schema check).
|
|
119
|
+
|
|
120
|
+
## What lives where
|
|
121
|
+
|
|
122
|
+
| Question | File |
|
|
123
|
+
|----------|------|
|
|
124
|
+
| What repos are in this ecosystem? | `ecosystem.json` |
|
|
125
|
+
| What are we shipping right now? | `.state/active-initiative` + `initiatives/<slug>.md` |
|
|
126
|
+
| What happened today? | `sessions/<today>.md` |
|
|
127
|
+
| What's the cross-repo dependency graph? | `ecosystem.json` `dependencies` |
|
|
128
|
+
| Where does a member repo's per-repo planning live? | Inside that member's own `specs/` (unchanged) |
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ecosystem helpers — pure, dependency-free.
|
|
5
|
+
*
|
|
6
|
+
* Provides four building blocks the rest of Phase 9 depends on:
|
|
7
|
+
*
|
|
8
|
+
* - findRoot(startPath) — locate the ecosystem root by walking up
|
|
9
|
+
* - loadManifest(rootPath) — read + parse ecosystem.json
|
|
10
|
+
* - listMembers(manifest) — convenience accessor
|
|
11
|
+
* - validateManifest(obj) — minimal schema check (no external deps)
|
|
12
|
+
*
|
|
13
|
+
* Schema validation is deliberately hand-rolled: momentum has a
|
|
14
|
+
* zero-dependency posture for the CLI (no ajv, no joi). The check
|
|
15
|
+
* is structural — type + required fields — and surfaces the first
|
|
16
|
+
* violation with a human-readable path. The full JSON Schema in
|
|
17
|
+
* `../schema/ecosystem.schema.json` remains the authoritative
|
|
18
|
+
* contract; this validator is its operational counterpart.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
// Default upper bound on parent-directory walk-up. Honored by both this
|
|
25
|
+
// module's findRoot and core/ecosystem/scripts/session-append.sh. The
|
|
26
|
+
// authoritative export lives in core/ecosystem/lib/state.js as of
|
|
27
|
+
// Phase 10 (ENH-022); we keep MAX_WALK_DEPTH as a backward-compat alias.
|
|
28
|
+
const MAX_PARENT_WALK_DEFAULT = 5;
|
|
29
|
+
const MAX_WALK_DEPTH = MAX_PARENT_WALK_DEFAULT;
|
|
30
|
+
const MANIFEST_FILENAME = 'ecosystem.json';
|
|
31
|
+
|
|
32
|
+
function resolveMaxParentWalk() {
|
|
33
|
+
const raw = process.env.MOMENTUM_MAX_PARENT_WALK;
|
|
34
|
+
if (raw === undefined || raw === '') return MAX_PARENT_WALK_DEFAULT;
|
|
35
|
+
const n = Number(raw);
|
|
36
|
+
if (!Number.isFinite(n) || n < 0) return MAX_PARENT_WALK_DEFAULT;
|
|
37
|
+
return Math.floor(n);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Per-process cache: absolute-startPath-prefix → resolved root (or null)
|
|
41
|
+
const rootCache = new Map();
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Walk up from `startPath` looking for a directory containing
|
|
45
|
+
* `ecosystem.json`. Returns the absolute root path or null.
|
|
46
|
+
* Bounded to `resolveMaxParentWalk()` parents (default 5, override via
|
|
47
|
+
* the `MOMENTUM_MAX_PARENT_WALK` env var) so a misconfigured caller
|
|
48
|
+
* can't scan the entire filesystem.
|
|
49
|
+
*
|
|
50
|
+
* Memoized: repeated calls with paths under the same root return
|
|
51
|
+
* instantly. Cache key is the absolute starting path.
|
|
52
|
+
*/
|
|
53
|
+
function findRoot(startPath) {
|
|
54
|
+
if (typeof startPath !== 'string' || startPath.length === 0) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const abs = path.resolve(startPath);
|
|
58
|
+
if (rootCache.has(abs)) {
|
|
59
|
+
return rootCache.get(abs);
|
|
60
|
+
}
|
|
61
|
+
const maxDepth = resolveMaxParentWalk();
|
|
62
|
+
let current = abs;
|
|
63
|
+
for (let i = 0; i <= maxDepth; i++) {
|
|
64
|
+
const candidate = path.join(current, MANIFEST_FILENAME);
|
|
65
|
+
let stat;
|
|
66
|
+
try {
|
|
67
|
+
stat = fs.statSync(candidate);
|
|
68
|
+
} catch (_err) {
|
|
69
|
+
stat = null;
|
|
70
|
+
}
|
|
71
|
+
if (stat && stat.isFile()) {
|
|
72
|
+
rootCache.set(abs, current);
|
|
73
|
+
return current;
|
|
74
|
+
}
|
|
75
|
+
const parent = path.dirname(current);
|
|
76
|
+
if (parent === current) break; // reached filesystem root
|
|
77
|
+
current = parent;
|
|
78
|
+
}
|
|
79
|
+
rootCache.set(abs, null);
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Read + parse the manifest at `rootPath/ecosystem.json`.
|
|
85
|
+
* Throws on missing file or invalid JSON; caller decides recovery.
|
|
86
|
+
*/
|
|
87
|
+
function loadManifest(rootPath) {
|
|
88
|
+
if (typeof rootPath !== 'string' || rootPath.length === 0) {
|
|
89
|
+
throw new TypeError('loadManifest: rootPath must be a non-empty string');
|
|
90
|
+
}
|
|
91
|
+
const file = path.join(rootPath, MANIFEST_FILENAME);
|
|
92
|
+
const raw = fs.readFileSync(file, 'utf8');
|
|
93
|
+
let parsed;
|
|
94
|
+
try {
|
|
95
|
+
parsed = JSON.parse(raw);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
throw new SyntaxError(`Invalid JSON in ${file}: ${err.message}`);
|
|
98
|
+
}
|
|
99
|
+
return parsed;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Returns the members array, defaulting to [] if absent.
|
|
104
|
+
* Does not validate; assumes caller already passed validateManifest.
|
|
105
|
+
*/
|
|
106
|
+
function listMembers(manifest) {
|
|
107
|
+
if (!manifest || typeof manifest !== 'object') return [];
|
|
108
|
+
return Array.isArray(manifest.members) ? manifest.members : [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Find a member by id. Returns the member object or null.
|
|
113
|
+
*/
|
|
114
|
+
function findMember(manifest, id) {
|
|
115
|
+
return listMembers(manifest).find((m) => m && m.id === id) || null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Hand-rolled structural validation. Returns
|
|
120
|
+
* { ok: true } on success
|
|
121
|
+
* { ok: false, errors: [{ path, message }] } on failure.
|
|
122
|
+
*
|
|
123
|
+
* Catches every issue (does not bail on first); the caller can render
|
|
124
|
+
* a useful error report. Authoritative schema is
|
|
125
|
+
* `core/ecosystem/schema/ecosystem.schema.json` — keep them in sync.
|
|
126
|
+
*/
|
|
127
|
+
function validateManifest(obj) {
|
|
128
|
+
const errors = [];
|
|
129
|
+
const slug = /^[a-z][a-z0-9-]*$/;
|
|
130
|
+
|
|
131
|
+
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
132
|
+
return { ok: false, errors: [{ path: '$', message: 'manifest must be a JSON object' }] };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// name
|
|
136
|
+
if (typeof obj.name !== 'string' || obj.name.length === 0) {
|
|
137
|
+
errors.push({ path: '$.name', message: 'required non-empty string' });
|
|
138
|
+
} else if (!slug.test(obj.name)) {
|
|
139
|
+
errors.push({ path: '$.name', message: 'must match /^[a-z][a-z0-9-]*$/' });
|
|
140
|
+
} else if (obj.name.length > 64) {
|
|
141
|
+
errors.push({ path: '$.name', message: 'must be at most 64 chars' });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// version
|
|
145
|
+
if (obj.version !== 1) {
|
|
146
|
+
errors.push({ path: '$.version', message: 'must be the integer 1 (no other schema version supported)' });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// created — optional, must be a valid ISO date if present
|
|
150
|
+
if (obj.created !== undefined) {
|
|
151
|
+
if (typeof obj.created !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(obj.created)) {
|
|
152
|
+
errors.push({ path: '$.created', message: 'must be an ISO-8601 date (YYYY-MM-DD)' });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// members
|
|
157
|
+
if (!Array.isArray(obj.members)) {
|
|
158
|
+
errors.push({ path: '$.members', message: 'required array' });
|
|
159
|
+
} else {
|
|
160
|
+
const seenIds = new Set();
|
|
161
|
+
obj.members.forEach((m, i) => {
|
|
162
|
+
const base = `$.members[${i}]`;
|
|
163
|
+
if (m === null || typeof m !== 'object' || Array.isArray(m)) {
|
|
164
|
+
errors.push({ path: base, message: 'must be an object' });
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (typeof m.id !== 'string' || m.id.length === 0) {
|
|
168
|
+
errors.push({ path: `${base}.id`, message: 'required non-empty string' });
|
|
169
|
+
} else {
|
|
170
|
+
if (!slug.test(m.id)) errors.push({ path: `${base}.id`, message: 'must match slug pattern' });
|
|
171
|
+
if (m.id.length > 64) errors.push({ path: `${base}.id`, message: 'must be at most 64 chars' });
|
|
172
|
+
if (seenIds.has(m.id)) errors.push({ path: `${base}.id`, message: `duplicate member id "${m.id}"` });
|
|
173
|
+
seenIds.add(m.id);
|
|
174
|
+
}
|
|
175
|
+
if (typeof m.path !== 'string' || m.path.length === 0) {
|
|
176
|
+
errors.push({ path: `${base}.path`, message: 'required non-empty string' });
|
|
177
|
+
}
|
|
178
|
+
const validRoles = ['platform', 'client', 'library', 'infra', 'bench', 'other'];
|
|
179
|
+
if (!validRoles.includes(m.role)) {
|
|
180
|
+
errors.push({ path: `${base}.role`, message: `must be one of: ${validRoles.join(', ')}` });
|
|
181
|
+
}
|
|
182
|
+
if (m.owns !== undefined && !isArrayOfStrings(m.owns)) {
|
|
183
|
+
errors.push({ path: `${base}.owns`, message: 'must be array of strings' });
|
|
184
|
+
}
|
|
185
|
+
if (m.consumes !== undefined && !isArrayOfStrings(m.consumes)) {
|
|
186
|
+
errors.push({ path: `${base}.consumes`, message: 'must be array of strings' });
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// dependencies — optional
|
|
192
|
+
if (obj.dependencies !== undefined) {
|
|
193
|
+
if (!Array.isArray(obj.dependencies)) {
|
|
194
|
+
errors.push({ path: '$.dependencies', message: 'must be array if present' });
|
|
195
|
+
} else {
|
|
196
|
+
const memberIds = new Set(
|
|
197
|
+
Array.isArray(obj.members) ? obj.members.map((m) => m && m.id).filter(Boolean) : [],
|
|
198
|
+
);
|
|
199
|
+
const validKinds = ['api-contract', 'library', 'deploy', 'build-time', 'other'];
|
|
200
|
+
obj.dependencies.forEach((d, i) => {
|
|
201
|
+
const base = `$.dependencies[${i}]`;
|
|
202
|
+
if (d === null || typeof d !== 'object' || Array.isArray(d)) {
|
|
203
|
+
errors.push({ path: base, message: 'must be an object' });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (typeof d.from !== 'string' || !memberIds.has(d.from)) {
|
|
207
|
+
errors.push({ path: `${base}.from`, message: 'must reference a known member id' });
|
|
208
|
+
}
|
|
209
|
+
if (typeof d.to !== 'string' || !memberIds.has(d.to)) {
|
|
210
|
+
errors.push({ path: `${base}.to`, message: 'must reference a known member id' });
|
|
211
|
+
}
|
|
212
|
+
if (!validKinds.includes(d.kind)) {
|
|
213
|
+
errors.push({ path: `${base}.kind`, message: `must be one of: ${validKinds.join(', ')}` });
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return errors.length === 0 ? { ok: true } : { ok: false, errors };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function isArrayOfStrings(v) {
|
|
223
|
+
return Array.isArray(v) && v.every((s) => typeof s === 'string');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Clear the findRoot memo cache. Tests use this; production code
|
|
228
|
+
* doesn't need to.
|
|
229
|
+
*/
|
|
230
|
+
function _clearRootCache() {
|
|
231
|
+
rootCache.clear();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = {
|
|
235
|
+
MAX_WALK_DEPTH,
|
|
236
|
+
MAX_PARENT_WALK_DEFAULT,
|
|
237
|
+
MANIFEST_FILENAME,
|
|
238
|
+
resolveMaxParentWalk,
|
|
239
|
+
findRoot,
|
|
240
|
+
loadManifest,
|
|
241
|
+
listMembers,
|
|
242
|
+
findMember,
|
|
243
|
+
validateManifest,
|
|
244
|
+
_clearRootCache,
|
|
245
|
+
};
|