@latysh-development/monolith 0.0.0-stage → 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +179 -2
- package/dist/brief/cli.d.ts +30 -0
- package/dist/brief/cli.d.ts.map +1 -0
- package/dist/brief/cli.js +85 -0
- package/dist/brief/collect.d.ts +17 -0
- package/dist/brief/collect.d.ts.map +1 -0
- package/dist/brief/collect.js +342 -0
- package/dist/brief/config.d.ts +40 -0
- package/dist/brief/config.d.ts.map +1 -0
- package/dist/brief/config.js +85 -0
- package/dist/brief/constants.d.ts +12 -0
- package/dist/brief/constants.d.ts.map +1 -0
- package/dist/brief/constants.js +11 -0
- package/dist/brief/docs.d.ts +53 -0
- package/dist/brief/docs.d.ts.map +1 -0
- package/dist/brief/docs.js +229 -0
- package/dist/brief/freshness.d.ts +104 -0
- package/dist/brief/freshness.d.ts.map +1 -0
- package/dist/brief/freshness.js +263 -0
- package/dist/brief/git-signals.d.ts +17 -0
- package/dist/brief/git-signals.d.ts.map +1 -0
- package/dist/brief/git-signals.js +101 -0
- package/dist/brief/index.d.ts +11 -0
- package/dist/brief/index.d.ts.map +1 -0
- package/dist/brief/index.js +5 -0
- package/dist/brief/reconcile.d.ts +75 -0
- package/dist/brief/reconcile.d.ts.map +1 -0
- package/dist/brief/reconcile.js +259 -0
- package/dist/brief/render.d.ts +67 -0
- package/dist/brief/render.d.ts.map +1 -0
- package/dist/brief/render.js +458 -0
- package/dist/brief/sanitize.d.ts +38 -0
- package/dist/brief/sanitize.d.ts.map +1 -0
- package/dist/brief/sanitize.js +48 -0
- package/dist/brief/selectors.d.ts +36 -0
- package/dist/brief/selectors.d.ts.map +1 -0
- package/dist/brief/selectors.js +75 -0
- package/dist/brief/story.d.ts +18 -0
- package/dist/brief/story.d.ts.map +1 -0
- package/dist/brief/story.js +59 -0
- package/dist/brief/types.d.ts +126 -0
- package/dist/brief/types.d.ts.map +1 -0
- package/dist/brief/types.js +11 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +199 -0
- package/dist/cli/init-args.d.ts +16 -0
- package/dist/cli/init-args.d.ts.map +1 -0
- package/dist/cli/init-args.js +24 -0
- package/dist/cli/ui-args.d.ts +23 -0
- package/dist/cli/ui-args.d.ts.map +1 -0
- package/dist/cli/ui-args.js +66 -0
- package/dist/cli/ui.d.ts +40 -0
- package/dist/cli/ui.d.ts.map +1 -0
- package/dist/cli/ui.js +180 -0
- package/dist/coordinatorium/checkpoint.d.ts +44 -0
- package/dist/coordinatorium/checkpoint.d.ts.map +1 -0
- package/dist/coordinatorium/checkpoint.js +124 -0
- package/dist/coordinatorium/cli.d.ts +39 -0
- package/dist/coordinatorium/cli.d.ts.map +1 -0
- package/dist/coordinatorium/cli.js +203 -0
- package/dist/coordinatorium/errors.d.ts +112 -0
- package/dist/coordinatorium/errors.d.ts.map +1 -0
- package/dist/coordinatorium/errors.js +145 -0
- package/dist/coordinatorium/index.d.ts +11 -0
- package/dist/coordinatorium/index.d.ts.map +1 -0
- package/dist/coordinatorium/index.js +5 -0
- package/dist/coordinatorium/lock.d.ts +187 -0
- package/dist/coordinatorium/lock.d.ts.map +1 -0
- package/dist/coordinatorium/lock.js +659 -0
- package/dist/coordinatorium/mission.d.ts +32 -0
- package/dist/coordinatorium/mission.d.ts.map +1 -0
- package/dist/coordinatorium/mission.js +87 -0
- package/dist/coordinatorium/state-keeper.d.ts +115 -0
- package/dist/coordinatorium/state-keeper.d.ts.map +1 -0
- package/dist/coordinatorium/state-keeper.js +396 -0
- package/dist/coordinatorium/types.d.ts +69 -0
- package/dist/coordinatorium/types.d.ts.map +1 -0
- package/dist/coordinatorium/types.js +27 -0
- package/dist/discovery/compose.d.ts +28 -0
- package/dist/discovery/compose.d.ts.map +1 -0
- package/dist/discovery/compose.js +159 -0
- package/dist/discovery/contracts.d.ts +211 -0
- package/dist/discovery/contracts.d.ts.map +1 -0
- package/dist/discovery/contracts.js +555 -0
- package/dist/discovery/docs.d.ts +278 -0
- package/dist/discovery/docs.d.ts.map +1 -0
- package/dist/discovery/docs.js +1182 -0
- package/dist/discovery/facts.d.ts +49 -0
- package/dist/discovery/facts.d.ts.map +1 -0
- package/dist/discovery/facts.js +541 -0
- package/dist/discovery/index.d.ts +8 -0
- package/dist/discovery/index.d.ts.map +1 -0
- package/dist/discovery/index.js +9 -0
- package/dist/discovery/orchestrate.d.ts +35 -0
- package/dist/discovery/orchestrate.d.ts.map +1 -0
- package/dist/discovery/orchestrate.js +32 -0
- package/dist/discovery/render.d.ts +36 -0
- package/dist/discovery/render.d.ts.map +1 -0
- package/dist/discovery/render.js +278 -0
- package/dist/discovery/traverse.d.ts +197 -0
- package/dist/discovery/traverse.d.ts.map +1 -0
- package/dist/discovery/traverse.js +510 -0
- package/dist/durable/assess.d.ts +105 -0
- package/dist/durable/assess.d.ts.map +1 -0
- package/dist/durable/assess.js +239 -0
- package/dist/durable/canonical.d.ts +25 -0
- package/dist/durable/canonical.d.ts.map +1 -0
- package/dist/durable/canonical.js +78 -0
- package/dist/durable/cli.d.ts +37 -0
- package/dist/durable/cli.d.ts.map +1 -0
- package/dist/durable/cli.js +141 -0
- package/dist/durable/errors.d.ts +192 -0
- package/dist/durable/errors.d.ts.map +1 -0
- package/dist/durable/errors.js +323 -0
- package/dist/durable/fs-store.d.ts +254 -0
- package/dist/durable/fs-store.d.ts.map +1 -0
- package/dist/durable/fs-store.js +1493 -0
- package/dist/durable/index.d.ts +20 -0
- package/dist/durable/index.d.ts.map +1 -0
- package/dist/durable/index.js +13 -0
- package/dist/durable/log.d.ts +33 -0
- package/dist/durable/log.d.ts.map +1 -0
- package/dist/durable/log.js +157 -0
- package/dist/durable/memory-store.d.ts +51 -0
- package/dist/durable/memory-store.d.ts.map +1 -0
- package/dist/durable/memory-store.js +302 -0
- package/dist/durable/projection.d.ts +35 -0
- package/dist/durable/projection.d.ts.map +1 -0
- package/dist/durable/projection.js +328 -0
- package/dist/durable/prune-check.d.ts +34 -0
- package/dist/durable/prune-check.d.ts.map +1 -0
- package/dist/durable/prune-check.js +72 -0
- package/dist/durable/register.d.ts +100 -0
- package/dist/durable/register.d.ts.map +1 -0
- package/dist/durable/register.js +532 -0
- package/dist/durable/retention.d.ts +49 -0
- package/dist/durable/retention.d.ts.map +1 -0
- package/dist/durable/retention.js +59 -0
- package/dist/durable/runtime.d.ts +499 -0
- package/dist/durable/runtime.d.ts.map +1 -0
- package/dist/durable/runtime.js +1288 -0
- package/dist/durable/seams.d.ts +44 -0
- package/dist/durable/seams.d.ts.map +1 -0
- package/dist/durable/seams.js +21 -0
- package/dist/durable/testing.d.ts +11 -0
- package/dist/durable/testing.d.ts.map +1 -0
- package/dist/durable/testing.js +25 -0
- package/dist/durable/types.d.ts +442 -0
- package/dist/durable/types.d.ts.map +1 -0
- package/dist/durable/types.js +75 -0
- package/dist/graph/builder.d.ts +11 -0
- package/dist/graph/builder.d.ts.map +1 -0
- package/dist/graph/builder.js +153 -0
- package/dist/graph/identity.d.ts +37 -0
- package/dist/graph/identity.d.ts.map +1 -0
- package/dist/graph/identity.js +46 -0
- package/dist/graph/index.d.ts +7 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +3 -0
- package/dist/graph/resolver.d.ts +41 -0
- package/dist/graph/resolver.d.ts.map +1 -0
- package/dist/graph/resolver.js +36 -0
- package/dist/graph/types.d.ts +42 -0
- package/dist/graph/types.d.ts.map +1 -0
- package/dist/graph/types.js +1 -0
- package/dist/http/index.d.ts +5 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/http/index.js +2 -0
- package/dist/http/server.d.ts +76 -0
- package/dist/http/server.d.ts.map +1 -0
- package/dist/http/server.js +195 -0
- package/dist/http/ui-assets.d.ts +22 -0
- package/dist/http/ui-assets.d.ts.map +1 -0
- package/dist/http/ui-assets.js +100 -0
- package/dist/ideas/cli.d.ts +33 -0
- package/dist/ideas/cli.d.ts.map +1 -0
- package/dist/ideas/cli.js +158 -0
- package/dist/ideas/format.d.ts +38 -0
- package/dist/ideas/format.d.ts.map +1 -0
- package/dist/ideas/format.js +194 -0
- package/dist/ideas/index.d.ts +6 -0
- package/dist/ideas/index.d.ts.map +1 -0
- package/dist/ideas/index.js +3 -0
- package/dist/ideas/store.d.ts +55 -0
- package/dist/ideas/store.d.ts.map +1 -0
- package/dist/ideas/store.js +262 -0
- package/dist/ideas/types.d.ts +54 -0
- package/dist/ideas/types.d.ts.map +1 -0
- package/dist/ideas/types.js +26 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/init/index.d.ts +12 -0
- package/dist/init/index.d.ts.map +1 -0
- package/dist/init/index.js +90 -0
- package/dist/init/prompts.d.ts +3 -0
- package/dist/init/prompts.d.ts.map +1 -0
- package/dist/init/prompts.js +47 -0
- package/dist/init/scaffold.d.ts +29 -0
- package/dist/init/scaffold.d.ts.map +1 -0
- package/dist/init/scaffold.js +208 -0
- package/dist/init/types.d.ts +14 -0
- package/dist/init/types.d.ts.map +1 -0
- package/dist/init/types.js +1 -0
- package/dist/managed/index.d.ts +73 -0
- package/dist/managed/index.d.ts.map +1 -0
- package/dist/managed/index.js +222 -0
- package/dist/readmodel/dashboard.d.ts +40 -0
- package/dist/readmodel/dashboard.d.ts.map +1 -0
- package/dist/readmodel/dashboard.js +236 -0
- package/dist/readmodel/index.d.ts +3 -0
- package/dist/readmodel/index.d.ts.map +1 -0
- package/dist/readmodel/index.js +1 -0
- package/dist/readmodel/types.d.ts +104 -0
- package/dist/readmodel/types.d.ts.map +1 -0
- package/dist/readmodel/types.js +15 -0
- package/dist/root/index.d.ts +186 -0
- package/dist/root/index.d.ts.map +1 -0
- package/dist/root/index.js +430 -0
- package/dist/root/safe-write.d.ts +268 -0
- package/dist/root/safe-write.d.ts.map +1 -0
- package/dist/root/safe-write.js +966 -0
- package/dist/root/write-artifacts.d.ts +20 -0
- package/dist/root/write-artifacts.d.ts.map +1 -0
- package/dist/root/write-artifacts.js +42 -0
- package/dist/scriptorium/compile.d.ts +4 -0
- package/dist/scriptorium/compile.d.ts.map +1 -0
- package/dist/scriptorium/compile.js +185 -0
- package/dist/scriptorium/frontmatter.d.ts +13 -0
- package/dist/scriptorium/frontmatter.d.ts.map +1 -0
- package/dist/scriptorium/frontmatter.js +90 -0
- package/dist/scriptorium/index.d.ts +3 -0
- package/dist/scriptorium/index.d.ts.map +1 -0
- package/dist/scriptorium/index.js +1 -0
- package/dist/scriptorium/markdown.d.ts +31 -0
- package/dist/scriptorium/markdown.d.ts.map +1 -0
- package/dist/scriptorium/markdown.js +88 -0
- package/dist/scriptorium/types.d.ts +78 -0
- package/dist/scriptorium/types.d.ts.map +1 -0
- package/dist/scriptorium/types.js +1 -0
- package/dist/types/enforcement.d.ts +11 -0
- package/dist/types/enforcement.d.ts.map +1 -0
- package/dist/types/enforcement.js +1 -0
- package/dist/types/integrity.d.ts +33 -0
- package/dist/types/integrity.d.ts.map +1 -0
- package/dist/types/integrity.js +1 -0
- package/dist/types/project-definition.d.ts +30 -0
- package/dist/types/project-definition.d.ts.map +1 -0
- package/dist/types/project-definition.js +1 -0
- package/dist/ui/assets/cosmic-glass-bg-Dwx47zPu.webp +0 -0
- package/dist/ui/assets/index-C4NhMaq7.js +28 -0
- package/dist/ui/assets/index-DMZ7ZvZY.css +1 -0
- package/dist/ui/index.html +13 -0
- package/dist/update/cli.d.ts +8 -0
- package/dist/update/cli.d.ts.map +1 -0
- package/dist/update/cli.js +115 -0
- package/dist/update/index.d.ts +78 -0
- package/dist/update/index.d.ts.map +1 -0
- package/dist/update/index.js +285 -0
- package/dist/validate/index.d.ts +6 -0
- package/dist/validate/index.d.ts.map +1 -0
- package/dist/validate/index.js +214 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +1 -0
- package/package.json +67 -4
- package/templates/.gitkeep +0 -0
- package/templates/default-instance/instruction/.cursorrules +59 -0
- package/templates/default-instance/instruction/.windsurfrules +59 -0
- package/templates/default-instance/instruction/CLAUDE.md +57 -0
- package/templates/default-instance/managed/agent.md +20 -0
- package/templates/default-instance/managed/bootstrap.md +7 -0
- package/templates/default-instance/managed/index-idea.md +5 -0
- package/templates/default-instance/managed/routing-idea.md +14 -0
- package/templates/default-instance/monolith/AGENT.md +1 -0
- package/templates/default-instance/monolith/context/boot/HANDOFF.md +49 -0
- package/templates/default-instance/monolith/context/boot/WORK_SESSION.md +30 -0
- package/templates/default-instance/monolith/context/execution/current-state.md +30 -0
- package/templates/default-instance/monolith/context/maps/dependency-map.md +38 -0
- package/templates/default-instance/monolith/context/maps/project-map.md +85 -0
- package/templates/default-instance/monolith/context/maps/roadmap-index.md +41 -0
- package/templates/default-instance/monolith/context/maps/routing-map.md +151 -0
- package/templates/default-instance/monolith/context/maps/semantic-map.md +75 -0
- package/templates/default-instance/monolith/context/models/model-registry.md +37 -0
- package/templates/default-instance/monolith/context/models/privacy-classification.md +35 -0
- package/templates/default-instance/monolith/context/templates/plan-template.md +56 -0
- package/templates/default-instance/monolith/governance/README.md +29 -0
- package/templates/default-instance/monolith/governance/RULE_SCHEMA.md +58 -0
- package/templates/default-instance/monolith/learning/INDEX.md +48 -0
- package/templates/default-instance/monolith/learning/injection-map.md +49 -0
- package/templates/default-instance/monolith/learning/taxonomy.md +30 -0
- package/templates/default-instance/monolith/memory/errors/README.md +26 -0
- package/templates/default-instance/monolith/memory/errors/SCHEMA.md +47 -0
- package/templates/default-instance/monolith/memory/errors/TAXONOMY.md +27 -0
- package/templates/default-instance/monolith/memory/long-term/architecture-principles.md +31 -0
- package/templates/default-instance/monolith/memory/long-term/decisions.md +36 -0
- package/templates/default-instance/monolith/memory/long-term/execution-history.md +20 -0
- package/templates/default-instance/monolith/memory/long-term/tech-debt.md +25 -0
- package/templates/default-instance/monolith/memory/working/active.md +41 -0
- package/templates/default-instance/monolith/memory/working/governance-result.md +0 -0
- package/templates/default-instance/monolith/memory/working/integrity-result.md +0 -0
- package/templates/default-instance/monolith/memory/working/story-index.md +28 -0
- package/templates/default-instance/monolith/protocols/boot/boot-sequence.md +59 -0
- package/templates/default-instance/monolith/protocols/cognition/cognitive-modes.md +90 -0
- package/templates/default-instance/monolith/protocols/cognition/dry-run.md +56 -0
- package/templates/default-instance/monolith/protocols/cognition/execution-trace-format.md +52 -0
- package/templates/default-instance/monolith/protocols/cognition/semantic-planning.md +51 -0
- package/templates/default-instance/monolith/protocols/execution/hybrid-execution.md +37 -0
- package/templates/default-instance/monolith/protocols/execution/local-llm-governance.md +42 -0
- package/templates/default-instance/monolith/protocols/governance/command-policy.md +51 -0
- package/templates/default-instance/monolith/protocols/governance/core-invariants.md +40 -0
- package/templates/default-instance/monolith/protocols/governance/memory-governance.md +54 -0
- package/templates/default-instance/monolith/protocols/governance/metadata-schema.md +62 -0
- package/templates/default-instance/monolith/protocols/governance/skills.md +53 -0
- package/templates/default-instance/monolith/protocols/governance/work-session.md +46 -0
- package/templates/default-instance/monolith/skills/INDEX.md +32 -0
- package/templates/default-instance/monolith/skills/current-status/SKILL.md +47 -0
- package/templates/default-instance/monolith/skills/finalize-story/SKILL.md +86 -0
- package/templates/default-instance/monolith/skills/global-status/SKILL.md +56 -0
- package/templates/default-instance/monolith/skills/idea-capture/SKILL.md +157 -0
- package/templates/default-instance/monolith/skills/self-improvement/SKILL.md +77 -0
- package/templates/default-instance/monolith/state.json +11 -0
- package/templates/default-instance/monolith/tests/INDEX.md +57 -0
- package/templates/default-instance/monolith/tests/scenarios/architecture-review.md +32 -0
- package/templates/default-instance/monolith/tests/scenarios/debug-failure.md +32 -0
- package/templates/default-instance/monolith/tests/scenarios/dry-run-activation.md +33 -0
- package/templates/default-instance/monolith/tests/scenarios/execution-trace-verification.md +31 -0
- package/templates/default-instance/monolith/tests/scenarios/finalize-story.md +35 -0
- package/templates/default-instance/monolith/tests/scenarios/implement-feature.md +36 -0
- package/templates/default-instance/monolith/tests/scenarios/local-llm-delegation.md +31 -0
- package/templates/default-instance/monolith/tests/scenarios/planning-request.md +32 -0
- package/templates/default-instance/monolith/tests/scenarios/sensitive-context-task.md +30 -0
- package/templates/default-instance/monolith/tests/scenarios/validator-verification.md +33 -0
- package/templates/default-instance/monolith/tests/validation/expected-behavior.md +70 -0
- package/templates/default-instance/monolith/tests/validators/INDEX.md +17 -0
- package/templates/default-instance/monolith/tests/validators/delegation-semantics.md +22 -0
- package/templates/default-instance/monolith/tests/validators/mode-consistency.md +25 -0
- package/templates/default-instance/monolith/tests/validators/privacy-routing.md +23 -0
- package/templates/default-instance/monolith/tests/validators/protocol-references.md +27 -0
- package/templates/default-instance/monolith/tests/validators/routing-integrity.md +30 -0
- package/templates/default-instance/projectroot/AGENTS.md +111 -0
- package/templates/default-instance/projectroot/monolith.config.json +32 -0
- package/templates/default-instance/projectroot/project.definition.mjs.template +41 -0
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded, safe project traversal (DISCOVERY-001B-1, ADR-021).
|
|
3
|
+
*
|
|
4
|
+
* Lists the directory tree of a project and nothing else: it never reads file
|
|
5
|
+
* content, never writes, never runs project code and never touches the network.
|
|
6
|
+
* It is the only code in Discovery that lists directories.
|
|
7
|
+
*
|
|
8
|
+
* What it does:
|
|
9
|
+
* - lists each directory through `opendir` + `Dir#readSync` (never `readdirSync`),
|
|
10
|
+
* one open handle at a time, so a hostile directory cannot be loaded whole before
|
|
11
|
+
* a cap applies; the handle is always closed (`finally`);
|
|
12
|
+
* - never follows a symlink (it is listed as kind `symlink` and counted; its target is
|
|
13
|
+
* never looked at); FIFOs, sockets and devices are listed as `other`, never opened;
|
|
14
|
+
* - excludes the real `<monolithRoot>` (resolved with `resolveMonolithRoot`, not by
|
|
15
|
+
* the literal name `.monolith`), a fixed list of dependency/build directories, nested
|
|
16
|
+
* repositories (a directory holding `.git`) and directories on another file system;
|
|
17
|
+
* - bounds the work and the retained result: directory entries READ (every listing counts,
|
|
18
|
+
* including the one of a directory later found to be unreadable, replaced or opaque),
|
|
19
|
+
* entries per directory, depth, wall-clock time and the UTF-8 bytes of the retained
|
|
20
|
+
* paths; every bound that fires yields `PARTIAL` with a typed diagnostic, never a
|
|
21
|
+
* silent cut. A nested repository is recognised by one `lstat` of `<dir>/.git` BEFORE
|
|
22
|
+
* the directory is listed, so its content is neither read nor counted;
|
|
23
|
+
* - is deterministic for an unchanged tree: names are ordered by UTF-16 code units
|
|
24
|
+
* (never `localeCompare`), breadth-first, and the result is sorted before it is
|
|
25
|
+
* returned. No clock value, absolute path or `readdir` order reaches the result.
|
|
26
|
+
*
|
|
27
|
+
* What it does NOT do (ADR-013, SEC-001B.3 open; TM-C unsupported): it does not prevent
|
|
28
|
+
* and does not reliably detect a process that replaces a directory (e.g. with a symlink)
|
|
29
|
+
* while the walk runs. The window runs from the `lstat` that queues a directory until
|
|
30
|
+
* the walk has listed it, i.e. the breadth-first distance in between. Comparing the
|
|
31
|
+
* identity (`dev`/`ino`) of a directory before and after listing it (`DIR_CHANGED`) may
|
|
32
|
+
* notice SOME replacements; a replacement that is put back before the second `lstat`
|
|
33
|
+
* goes unnoticed, and names read from the substitute can then appear as COMPLETE.
|
|
34
|
+
* Do not run it on a tree an untrusted process is modifying.
|
|
35
|
+
*
|
|
36
|
+
* Root: `projectRoot` must be absolute and is the caller's explicit choice. A symlink as
|
|
37
|
+
* root is accepted and its target is walked (nothing below the root is ever followed, so
|
|
38
|
+
* the boundary is the directory the caller named). A missing, non-directory, unreadable
|
|
39
|
+
* or otherwise unresolvable root throws `ROOT_UNSAFE` (from `resolveMonolithRoot`, before
|
|
40
|
+
* any listing); `ROOT_NOT_DIRECTORY` is the defensive second check. Errors other than a
|
|
41
|
+
* root-containment error from `resolveMonolithRoot` propagate unchanged.
|
|
42
|
+
*
|
|
43
|
+
* Nested MONOLITH instances: only the actual `<monolithRoot>` is excluded. A sub-project
|
|
44
|
+
* that has its own `.monolith` stays part of the inventory (its sources belong to this
|
|
45
|
+
* project); that directory is not recognised by name. Excluding the confirmed internals
|
|
46
|
+
* of nested instances is a decision for DISCOVERY-001B-2.
|
|
47
|
+
*
|
|
48
|
+
* The time budget is checked between entries; a synchronous syscall that hangs in the
|
|
49
|
+
* kernel (e.g. a dead network mount) is not interrupted.
|
|
50
|
+
*/
|
|
51
|
+
import { lstatSync, opendirSync, statSync, constants } from 'fs';
|
|
52
|
+
import { join } from 'path';
|
|
53
|
+
import { RootContainmentError, resolveMonolithRoot } from '../root/index.js';
|
|
54
|
+
/** Starting values (ADR-021 Q-10); validated by the 001B-1 benchmark, see the story report. */
|
|
55
|
+
export const DEFAULT_TRAVERSAL_LIMITS = Object.freeze({
|
|
56
|
+
maxEntries: 50_000,
|
|
57
|
+
maxEntriesPerDir: 10_000,
|
|
58
|
+
maxDepth: 32,
|
|
59
|
+
deadlineMs: 30_000,
|
|
60
|
+
// Measured (DISCOVERY-001B-1-F): 50 000 paths of 3.6 KB each retained ~188 MiB of heap;
|
|
61
|
+
// real trees average well under 100 B per path (a few MiB at 50 000 entries).
|
|
62
|
+
maxPathBytes: 16 * 1024 * 1024,
|
|
63
|
+
});
|
|
64
|
+
/** Directories never entered, matched by name at any depth. */
|
|
65
|
+
export const EXCLUDED_DIR_NAMES = [
|
|
66
|
+
'node_modules',
|
|
67
|
+
'.git',
|
|
68
|
+
'dist',
|
|
69
|
+
'build',
|
|
70
|
+
'.next',
|
|
71
|
+
'out',
|
|
72
|
+
'coverage',
|
|
73
|
+
'.venv',
|
|
74
|
+
'venv',
|
|
75
|
+
'__pycache__',
|
|
76
|
+
'target',
|
|
77
|
+
'vendor',
|
|
78
|
+
];
|
|
79
|
+
/** Directories never entered, matched by project-relative path. */
|
|
80
|
+
export const EXCLUDED_DIR_PATHS = ['.claude/worktrees'];
|
|
81
|
+
/** Per code: how many individual paths are listed before only the total is kept. */
|
|
82
|
+
const MAX_DIAGNOSTIC_PATHS = 20;
|
|
83
|
+
const MAX_NAME_BYTES = 255;
|
|
84
|
+
/**
|
|
85
|
+
* Closed list. `affectsCompleteness` codes make the result PARTIAL (something inside
|
|
86
|
+
* the declared scope could not be done). The others report the scope policy.
|
|
87
|
+
* An unknown code is treated as affecting completeness (fail closed).
|
|
88
|
+
*/
|
|
89
|
+
const DIAGNOSTIC_AFFECTS_COMPLETENESS = {
|
|
90
|
+
LIMIT_ENTRIES: true,
|
|
91
|
+
LIMIT_DEPTH: true,
|
|
92
|
+
LIMIT_DIR_ENTRIES: true,
|
|
93
|
+
LIMIT_DEADLINE: true,
|
|
94
|
+
LIMIT_PATH_BYTES: true,
|
|
95
|
+
DIR_UNREADABLE: true,
|
|
96
|
+
DIR_VANISHED: true,
|
|
97
|
+
DIR_CHANGED: true,
|
|
98
|
+
DIR_ERROR: true,
|
|
99
|
+
NAME_UNREPRESENTABLE: true,
|
|
100
|
+
ENTRY_UNCLASSIFIED: true,
|
|
101
|
+
EXCLUDED_DIR: false,
|
|
102
|
+
MONOLITH_ROOT_EXCLUDED: false,
|
|
103
|
+
SYMLINK_SKIPPED: false,
|
|
104
|
+
NESTED_REPO_SKIPPED: false,
|
|
105
|
+
MOUNT_SKIPPED: false,
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Whether a code is a function of the tree and the parameters alone. A `false` code means a
|
|
109
|
+
* second walk of the same path may return another result (clock speed, a tree being modified,
|
|
110
|
+
* a transient I/O failure), so the result is not `reproducible`. Stable codes include the
|
|
111
|
+
* resource limits that cut a deterministic prefix and permission errors on an unchanged tree.
|
|
112
|
+
*/
|
|
113
|
+
const DIAGNOSTIC_IS_STABLE = {
|
|
114
|
+
LIMIT_ENTRIES: true,
|
|
115
|
+
LIMIT_DEPTH: true,
|
|
116
|
+
LIMIT_DIR_ENTRIES: true,
|
|
117
|
+
LIMIT_DEADLINE: false,
|
|
118
|
+
LIMIT_PATH_BYTES: true,
|
|
119
|
+
DIR_UNREADABLE: true,
|
|
120
|
+
DIR_VANISHED: false,
|
|
121
|
+
DIR_CHANGED: false,
|
|
122
|
+
DIR_ERROR: false,
|
|
123
|
+
NAME_UNREPRESENTABLE: true,
|
|
124
|
+
ENTRY_UNCLASSIFIED: false,
|
|
125
|
+
EXCLUDED_DIR: true,
|
|
126
|
+
MONOLITH_ROOT_EXCLUDED: true,
|
|
127
|
+
SYMLINK_SKIPPED: true,
|
|
128
|
+
NESTED_REPO_SKIPPED: true,
|
|
129
|
+
MOUNT_SKIPPED: true,
|
|
130
|
+
};
|
|
131
|
+
/** The traversal cannot start or cannot be trusted; no result exists (FAILED). */
|
|
132
|
+
export class TraversalError extends Error {
|
|
133
|
+
code;
|
|
134
|
+
constructor(code, message, options) {
|
|
135
|
+
super(message, options);
|
|
136
|
+
this.name = 'TraversalError';
|
|
137
|
+
this.code = code;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const realFs = {
|
|
141
|
+
opendir: (path) => opendirSync(path),
|
|
142
|
+
lstat: (path) => lstatSync(path, { bigint: true }),
|
|
143
|
+
stat: (path) => statSync(path, { bigint: true }),
|
|
144
|
+
};
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
function resolveLimits(partial) {
|
|
147
|
+
const limits = { ...DEFAULT_TRAVERSAL_LIMITS, ...(partial ?? {}) };
|
|
148
|
+
for (const key of Object.keys(DEFAULT_TRAVERSAL_LIMITS)) {
|
|
149
|
+
const v = limits[key];
|
|
150
|
+
if (typeof v !== 'number' || !Number.isInteger(v) || v < 1) {
|
|
151
|
+
throw new TraversalError('INVALID_LIMITS', `limit "${key}" must be a positive integer (got ${String(v)}).`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return limits;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Why a name cannot be represented faithfully, or `null`: U+FFFD (invalid UTF-8 was replaced),
|
|
158
|
+
* NUL, C0/C1 controls, over-long, empty. The reason is a fixed phrase; the name itself never
|
|
159
|
+
* reaches a diagnostic.
|
|
160
|
+
*/
|
|
161
|
+
function nameProblem(name) {
|
|
162
|
+
if (name.length === 0)
|
|
163
|
+
return 'empty';
|
|
164
|
+
if (Buffer.byteLength(name, 'utf8') > MAX_NAME_BYTES)
|
|
165
|
+
return `longer than ${MAX_NAME_BYTES} bytes`;
|
|
166
|
+
for (let i = 0; i < name.length; i++) {
|
|
167
|
+
const c = name.charCodeAt(i);
|
|
168
|
+
if (c < 0x20 || c === 0x7f || (c >= 0x80 && c <= 0x9f))
|
|
169
|
+
return 'contains a control character';
|
|
170
|
+
if (c === 0xfffd)
|
|
171
|
+
return 'contains U+FFFD (invalid UTF-8 or a literal replacement character)';
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
/** UTF-16 code-unit order (the default `<`), not locale order. */
|
|
176
|
+
function compareUnits(a, b) {
|
|
177
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
178
|
+
}
|
|
179
|
+
class DiagnosticLog {
|
|
180
|
+
byCode = new Map();
|
|
181
|
+
add(code, message, path) {
|
|
182
|
+
let slot = this.byCode.get(code);
|
|
183
|
+
if (!slot) {
|
|
184
|
+
slot = { total: 0, items: [] };
|
|
185
|
+
this.byCode.set(code, slot);
|
|
186
|
+
}
|
|
187
|
+
slot.total++;
|
|
188
|
+
if (slot.items.length < MAX_DIAGNOSTIC_PATHS) {
|
|
189
|
+
slot.items.push(path === undefined ? { code, message } : { code, path, message });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
has(code) {
|
|
193
|
+
return this.byCode.has(code);
|
|
194
|
+
}
|
|
195
|
+
isStable() {
|
|
196
|
+
for (const code of this.byCode.keys()) {
|
|
197
|
+
if (!DIAGNOSTIC_IS_STABLE[code])
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
affectsCompleteness() {
|
|
203
|
+
for (const code of this.byCode.keys()) {
|
|
204
|
+
if (DIAGNOSTIC_AFFECTS_COMPLETENESS[code] !== false)
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
toSortedList() {
|
|
210
|
+
const out = [];
|
|
211
|
+
for (const [code, slot] of this.byCode) {
|
|
212
|
+
out.push(...slot.items);
|
|
213
|
+
if (slot.total > slot.items.length) {
|
|
214
|
+
out.push({ code, count: slot.total, message: `${slot.total} occurrences in total; the first ${slot.items.length} are listed.` });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const rank = (d) => (DIAGNOSTIC_AFFECTS_COMPLETENESS[d.code] !== false ? 0 : 1);
|
|
218
|
+
return out.sort((a, b) => rank(a) - rank(b) ||
|
|
219
|
+
compareUnits(a.code, b.code) ||
|
|
220
|
+
compareUnits(a.path ?? '', b.path ?? '') ||
|
|
221
|
+
compareUnits(a.message, b.message));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* The only way a file-system error reaches a diagnostic or a thrown message: its `code`, and
|
|
226
|
+
* only when it looks like an errno name. `message`, `stack` and any other property of an
|
|
227
|
+
* error are untrusted (a seam or exotic file system can put a path or control characters in
|
|
228
|
+
* them) and are never copied.
|
|
229
|
+
*/
|
|
230
|
+
const SAFE_ERRNO = /^[A-Z][A-Z0-9_]{1,31}$/;
|
|
231
|
+
const UNKNOWN_ERROR = 'UNKNOWN_ERROR';
|
|
232
|
+
function errno(err) {
|
|
233
|
+
let code;
|
|
234
|
+
try {
|
|
235
|
+
code = err?.code;
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return UNKNOWN_ERROR;
|
|
239
|
+
}
|
|
240
|
+
return typeof code === 'string' && SAFE_ERRNO.test(code) ? code : UNKNOWN_ERROR;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* List one directory with two hard caps and always close the handle. Reading stops at the
|
|
244
|
+
* first entry beyond `maxEntriesPerDir` ('opaque') or beyond `budgetLeft` ('budget'; the
|
|
245
|
+
* directory does not fit in what `maxEntries` leaves). Both outcomes depend only on how many
|
|
246
|
+
* entries the directory has, never on the order they are returned in. When the caps coincide
|
|
247
|
+
* the directory is reported as opaque.
|
|
248
|
+
*/
|
|
249
|
+
function listDirectory(fs, abs, maxEntriesPerDir, budgetLeft) {
|
|
250
|
+
let handle;
|
|
251
|
+
try {
|
|
252
|
+
handle = fs.opendir(abs);
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
return failureOf(err, 0);
|
|
256
|
+
}
|
|
257
|
+
const dirents = [];
|
|
258
|
+
const cap = Math.min(maxEntriesPerDir, budgetLeft);
|
|
259
|
+
let outcome;
|
|
260
|
+
try {
|
|
261
|
+
for (;;) {
|
|
262
|
+
const dirent = handle.readSync();
|
|
263
|
+
if (dirent === null)
|
|
264
|
+
break;
|
|
265
|
+
if (dirents.length >= cap) {
|
|
266
|
+
// One entry more than the cap: stop reading, the directory is not enumerated.
|
|
267
|
+
const opaque = maxEntriesPerDir <= budgetLeft;
|
|
268
|
+
return opaque
|
|
269
|
+
? { ok: false, read: dirents.length + 1, reason: 'opaque', detail: `more than ${maxEntriesPerDir} entries` }
|
|
270
|
+
: { ok: false, read: dirents.length + 1, reason: 'budget', detail: `more than ${budgetLeft} entries left in the budget` };
|
|
271
|
+
}
|
|
272
|
+
dirents.push({ name: dirent.name, dirent });
|
|
273
|
+
}
|
|
274
|
+
outcome = { ok: true, read: dirents.length, dirents };
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
outcome = failureOf(err, dirents.length);
|
|
278
|
+
}
|
|
279
|
+
finally {
|
|
280
|
+
try {
|
|
281
|
+
handle.closeSync();
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
// A failed close of a read-only handle changes nothing we returned.
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return outcome;
|
|
288
|
+
}
|
|
289
|
+
function failureOf(err, read) {
|
|
290
|
+
const code = errno(err);
|
|
291
|
+
if (code === 'EACCES' || code === 'EPERM')
|
|
292
|
+
return { ok: false, read, reason: 'unreadable', detail: `${code}` };
|
|
293
|
+
if (code === 'ENOENT' || code === 'ENOTDIR')
|
|
294
|
+
return { ok: false, read, reason: 'vanished', detail: `${code}` };
|
|
295
|
+
return { ok: false, read, reason: 'error', detail: code };
|
|
296
|
+
}
|
|
297
|
+
/** Whether `<abs>/.git` exists (any type, not followed). One `lstat`, no listing; any failure means "not known to exist". */
|
|
298
|
+
function hasGitEntry(fs, abs) {
|
|
299
|
+
try {
|
|
300
|
+
fs.lstat(join(abs, '.git'));
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/** Throws when the type is not reported and `lstat` fails: the caller must not invent a kind. */
|
|
308
|
+
function direntKind(fs, dirent, abs) {
|
|
309
|
+
if (dirent.isSymbolicLink())
|
|
310
|
+
return 'symlink';
|
|
311
|
+
if (dirent.isDirectory())
|
|
312
|
+
return 'directory';
|
|
313
|
+
if (dirent.isFile())
|
|
314
|
+
return 'file';
|
|
315
|
+
// DT_UNKNOWN: the file system does not report a type in the listing; ask, without following.
|
|
316
|
+
const st = fs.lstat(abs);
|
|
317
|
+
if (st.isSymbolicLink())
|
|
318
|
+
return 'symlink';
|
|
319
|
+
if (st.isDirectory())
|
|
320
|
+
return 'directory';
|
|
321
|
+
if (st.isFile())
|
|
322
|
+
return 'file';
|
|
323
|
+
return 'other';
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Walk the project at `projectRoot` and return its structure.
|
|
327
|
+
*
|
|
328
|
+
* `projectRoot` is the explicit, caller-resolved project directory (for the CLI: `cwd`).
|
|
329
|
+
* The instance root is resolved with `resolveMonolithRoot` and excluded. Throws
|
|
330
|
+
* `TraversalError` when the walk cannot start or be trusted; otherwise returns a
|
|
331
|
+
* result that says whether it is `COMPLETE` or `PARTIAL`.
|
|
332
|
+
*/
|
|
333
|
+
export function traverseProject(projectRoot, options = {}) {
|
|
334
|
+
const flags = options.platformFlags ?? { noFollow: constants.O_NOFOLLOW, nonBlock: constants.O_NONBLOCK };
|
|
335
|
+
if (typeof flags.noFollow !== 'number' || flags.noFollow === 0 || typeof flags.nonBlock !== 'number' || flags.nonBlock === 0) {
|
|
336
|
+
throw new TraversalError('UNSUPPORTED_PLATFORM', 'this platform lacks O_NOFOLLOW/O_NONBLOCK; Discovery fails closed here (ADR-021).');
|
|
337
|
+
}
|
|
338
|
+
const limits = resolveLimits(options.limits);
|
|
339
|
+
const fs = options.fs ?? realFs;
|
|
340
|
+
const now = options.now ?? (() => Number(process.hrtime.bigint() / 1000000n));
|
|
341
|
+
let monolithRoot;
|
|
342
|
+
try {
|
|
343
|
+
monolithRoot = resolveMonolithRoot(projectRoot).monolithRoot;
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
if (err instanceof RootContainmentError)
|
|
347
|
+
throw new TraversalError('ROOT_UNSAFE', err.message, { cause: err });
|
|
348
|
+
throw err;
|
|
349
|
+
}
|
|
350
|
+
let rootIdentity;
|
|
351
|
+
try {
|
|
352
|
+
rootIdentity = fs.stat(projectRoot);
|
|
353
|
+
}
|
|
354
|
+
catch (err) {
|
|
355
|
+
throw new TraversalError('ROOT_NOT_DIRECTORY', `project root is not accessible (${errno(err)}).`);
|
|
356
|
+
}
|
|
357
|
+
if (!rootIdentity.isDirectory())
|
|
358
|
+
throw new TraversalError('ROOT_NOT_DIRECTORY', 'project root is not a directory.');
|
|
359
|
+
const startedAt = now();
|
|
360
|
+
const diagnostics = new DiagnosticLog();
|
|
361
|
+
const entries = [];
|
|
362
|
+
const stats = { entries: 0, files: 0, directories: 0, symlinks: 0, other: 0, examined: 0, pathBytes: 0, listedDirectories: 0 };
|
|
363
|
+
let stopped = false;
|
|
364
|
+
let deadlineFired = false;
|
|
365
|
+
const overDeadline = () => {
|
|
366
|
+
if (now() - startedAt > limits.deadlineMs) {
|
|
367
|
+
if (!deadlineFired)
|
|
368
|
+
diagnostics.add('LIMIT_DEADLINE', `time budget of ${limits.deadlineMs} ms exhausted; the walk stopped.`);
|
|
369
|
+
deadlineFired = true;
|
|
370
|
+
stopped = true;
|
|
371
|
+
}
|
|
372
|
+
return stopped;
|
|
373
|
+
};
|
|
374
|
+
const queue = [{ rel: '', depth: 0, identity: rootIdentity }];
|
|
375
|
+
for (let qi = 0; qi < queue.length && !overDeadline(); qi++) {
|
|
376
|
+
const dir = queue[qi];
|
|
377
|
+
const abs = dir.rel === '' ? projectRoot : join(projectRoot, dir.rel);
|
|
378
|
+
const label = dir.rel === '' ? '.' : dir.rel;
|
|
379
|
+
// A nested repository is recognised without listing it, so its content is neither read nor counted.
|
|
380
|
+
if (dir.rel !== '' && hasGitEntry(fs, abs)) {
|
|
381
|
+
diagnostics.add('NESTED_REPO_SKIPPED', 'nested repository or worktree (contains .git); not entered.', label);
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
const listed = listDirectory(fs, abs, limits.maxEntriesPerDir, limits.maxEntries - stats.examined);
|
|
385
|
+
// Every listing is charged for what it read, whatever becomes of it (kept, unreadable, replaced, opaque).
|
|
386
|
+
stats.examined += listed.read;
|
|
387
|
+
if (stats.examined > limits.maxEntries) {
|
|
388
|
+
diagnostics.add('LIMIT_ENTRIES', `more than ${limits.maxEntries} entries read; the walk stopped.`);
|
|
389
|
+
stopped = true;
|
|
390
|
+
}
|
|
391
|
+
if (!listed.ok) {
|
|
392
|
+
if (listed.reason === 'opaque') {
|
|
393
|
+
diagnostics.add('LIMIT_DIR_ENTRIES', `directory has more than ${limits.maxEntriesPerDir} entries; not enumerated.`, label);
|
|
394
|
+
}
|
|
395
|
+
else if (listed.reason === 'budget') {
|
|
396
|
+
// LIMIT_ENTRIES was recorded above: this directory did not fit in the remaining budget.
|
|
397
|
+
}
|
|
398
|
+
else if (listed.reason === 'unreadable') {
|
|
399
|
+
diagnostics.add('DIR_UNREADABLE', `directory cannot be read (${listed.detail}).`, label);
|
|
400
|
+
}
|
|
401
|
+
else if (listed.reason === 'vanished') {
|
|
402
|
+
diagnostics.add('DIR_VANISHED', `directory disappeared during the walk (${listed.detail}); the tree is being modified.`, label);
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
diagnostics.add('DIR_ERROR', `directory cannot be listed (${listed.detail}).`, label);
|
|
406
|
+
}
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
// Identity after listing must match the identity before (may notice a replacement; does not prevent one; see header).
|
|
410
|
+
let after;
|
|
411
|
+
try {
|
|
412
|
+
after = dir.rel === '' ? fs.stat(abs) : fs.lstat(abs);
|
|
413
|
+
}
|
|
414
|
+
catch (err) {
|
|
415
|
+
diagnostics.add('DIR_VANISHED', `directory disappeared during the walk (${errno(err)}); the tree is being modified.`, label);
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (after.dev !== dir.identity.dev || after.ino !== dir.identity.ino || (dir.rel !== '' && !after.isDirectory())) {
|
|
419
|
+
diagnostics.add('DIR_CHANGED', 'directory was replaced while it was being listed; its contents are discarded.', label);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
stats.listedDirectories++;
|
|
423
|
+
// The probe above normally catches this; the listing is the second look (a .git that appeared meanwhile).
|
|
424
|
+
const hasGit = listed.dirents.some((d) => d.name === '.git');
|
|
425
|
+
if (dir.rel !== '' && hasGit) {
|
|
426
|
+
diagnostics.add('NESTED_REPO_SKIPPED', 'nested repository or worktree (contains .git); not entered.', label);
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
const sorted = listed.dirents.sort((a, b) => compareUnits(a.name, b.name));
|
|
430
|
+
for (const { name, dirent } of sorted) {
|
|
431
|
+
if (overDeadline())
|
|
432
|
+
break;
|
|
433
|
+
const rel = dir.rel === '' ? name : `${dir.rel}/${name}`;
|
|
434
|
+
const problem = nameProblem(name);
|
|
435
|
+
if (problem !== null) {
|
|
436
|
+
diagnostics.add('NAME_UNREPRESENTABLE', `an entry whose name ${problem} was excluded; its name is not shown.`, label);
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (rel === monolithRoot) {
|
|
440
|
+
diagnostics.add('MONOLITH_ROOT_EXCLUDED', 'the MONOLITH instance root is not part of the inventory.', rel);
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
let kind;
|
|
444
|
+
try {
|
|
445
|
+
kind = direntKind(fs, dirent, join(projectRoot, rel));
|
|
446
|
+
}
|
|
447
|
+
catch (err) {
|
|
448
|
+
// No type was reported and lstat failed: do not state a type, and do not hide the cause.
|
|
449
|
+
diagnostics.add('ENTRY_UNCLASSIFIED', `entry type could not be determined (${errno(err)}); not listed.`, rel);
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
if (kind === 'directory' && (EXCLUDED_DIR_NAMES.includes(name) || EXCLUDED_DIR_PATHS.includes(rel))) {
|
|
453
|
+
diagnostics.add('EXCLUDED_DIR', `excluded directory "${name}".`, rel);
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
const bytes = Buffer.byteLength(rel, 'utf8');
|
|
457
|
+
if (stats.pathBytes + bytes > limits.maxPathBytes) {
|
|
458
|
+
diagnostics.add('LIMIT_PATH_BYTES', `retained paths would exceed ${limits.maxPathBytes} bytes; the walk stopped.`);
|
|
459
|
+
stopped = true;
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
stats.pathBytes += bytes;
|
|
463
|
+
entries.push({ path: rel, kind });
|
|
464
|
+
stats.entries++;
|
|
465
|
+
if (kind === 'file')
|
|
466
|
+
stats.files++;
|
|
467
|
+
else if (kind === 'directory')
|
|
468
|
+
stats.directories++;
|
|
469
|
+
else if (kind === 'symlink') {
|
|
470
|
+
stats.symlinks++;
|
|
471
|
+
diagnostics.add('SYMLINK_SKIPPED', 'symbolic link listed, not followed.', rel);
|
|
472
|
+
}
|
|
473
|
+
else
|
|
474
|
+
stats.other++;
|
|
475
|
+
if (kind !== 'directory')
|
|
476
|
+
continue;
|
|
477
|
+
const depth = dir.depth + 1;
|
|
478
|
+
if (depth >= limits.maxDepth) {
|
|
479
|
+
diagnostics.add('LIMIT_DEPTH', `directory is at the maximum depth of ${limits.maxDepth}; not entered.`, rel);
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
let identity;
|
|
483
|
+
try {
|
|
484
|
+
identity = fs.lstat(join(projectRoot, rel));
|
|
485
|
+
}
|
|
486
|
+
catch (err) {
|
|
487
|
+
diagnostics.add('DIR_VANISHED', `directory disappeared during the walk (${errno(err)}); the tree is being modified.`, rel);
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
if (!identity.isDirectory()) {
|
|
491
|
+
diagnostics.add('DIR_CHANGED', 'entry listed as a directory is no longer one; not entered.', rel);
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
if (identity.dev !== rootIdentity.dev) {
|
|
495
|
+
diagnostics.add('MOUNT_SKIPPED', 'directory is on another file system; not entered.', rel);
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
queue.push({ rel, depth, identity });
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
entries.sort((a, b) => compareUnits(a.path, b.path));
|
|
502
|
+
return {
|
|
503
|
+
completeness: diagnostics.affectsCompleteness() ? 'PARTIAL' : 'COMPLETE',
|
|
504
|
+
reproducible: diagnostics.isStable(),
|
|
505
|
+
limits,
|
|
506
|
+
stats,
|
|
507
|
+
entries,
|
|
508
|
+
diagnostics: diagnostics.toSortedList(),
|
|
509
|
+
};
|
|
510
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resume assessment — "may I simply carry on?"
|
|
3
|
+
*
|
|
4
|
+
* A pure, deterministic function of a task's state and log (no clock except
|
|
5
|
+
* the `now` the caller passes, no I/O, no LLM). It answers the question every
|
|
6
|
+
* resumed agentic task has to answer, and classifies the answer into the
|
|
7
|
+
* three continuation scenarios:
|
|
8
|
+
*
|
|
9
|
+
* - `direct` : results are in, nothing the plan relied on changed, the next
|
|
10
|
+
* step is known — continue without further reasoning.
|
|
11
|
+
* - `adaptive` : results are in, the world moved, but nothing *blocking* was
|
|
12
|
+
* invalidated — continue, taking the listed changes into account.
|
|
13
|
+
* - `recovery` : a blocking assumption broke, an effect is in doubt, an awaited
|
|
14
|
+
* result never came, or the next step is unknown — the plan can't
|
|
15
|
+
* be trusted; hand the resume packet to a reasoning component.
|
|
16
|
+
*
|
|
17
|
+
* plus `waiting` (nothing runnable yet; more results are expected) and
|
|
18
|
+
* `closed` (terminal). The classification is conservative: a doubt is never
|
|
19
|
+
* resolved in favour of `direct`.
|
|
20
|
+
*
|
|
21
|
+
* The assessment compares *recorded* assumptions with the *current* values of
|
|
22
|
+
* the facts they were about. Current values come from `fact.observed` events
|
|
23
|
+
* (other agents/systems reporting) and from `observations` the caller supplies
|
|
24
|
+
* live; the live value wins. Durable State does not know how to look at the
|
|
25
|
+
* world — the application does — but it knows what the plan relied on.
|
|
26
|
+
*/
|
|
27
|
+
import { type Json } from './canonical.js';
|
|
28
|
+
import type { AwaitState, PlanStepState, TaskEvent, TaskState } from './types.js';
|
|
29
|
+
export type ResumeMode = 'closed' | 'waiting' | 'direct' | 'adaptive' | 'recovery';
|
|
30
|
+
export type ReasonCode = 'BLOCKING_ASSUMPTION_CHANGED' | 'BLOCKING_ASSUMPTION_UNVERIFIED' | 'ADVISORY_ASSUMPTION_CHANGED' | 'EFFECT_IN_DOUBT' | 'AWAIT_EXPIRED' | 'LATE_RESULT' | 'PLAN_STUCK' | 'NO_PLAN_YET' | 'NO_PLAN_BUT_PROGRESS' | 'WORLD_CHANGED_SINCE_SEND' | 'REPLANNED_SINCE_SEND' | 'LEASE_HELD'
|
|
31
|
+
/** A task this one requires (parentTaskId / dependsOn) was pruned or is missing: the task must not run. */
|
|
32
|
+
| 'REQUIRED_TASK_GONE'
|
|
33
|
+
/** A task this one requires is in an unfinished prune: the task does not run now, and is NOT cancelled. */
|
|
34
|
+
| 'REQUIRED_TASK_PRUNE_PENDING';
|
|
35
|
+
export interface Reason {
|
|
36
|
+
code: ReasonCode;
|
|
37
|
+
severity: 'recovery' | 'adaptive' | 'info';
|
|
38
|
+
detail: string;
|
|
39
|
+
refs: string[];
|
|
40
|
+
}
|
|
41
|
+
export interface AssumptionChange {
|
|
42
|
+
key: string;
|
|
43
|
+
criticality: 'blocking' | 'advisory';
|
|
44
|
+
statement: string;
|
|
45
|
+
was: Json | null;
|
|
46
|
+
now: Json | null;
|
|
47
|
+
source: 'live' | 'recorded';
|
|
48
|
+
}
|
|
49
|
+
export interface AssessOptions {
|
|
50
|
+
now: Date;
|
|
51
|
+
/** Current values of facts, keyed like the assumptions. Wins over recorded observations. */
|
|
52
|
+
observations?: Record<string, unknown>;
|
|
53
|
+
/** What an unverifiable *blocking* assumption means. Default `'adaptive'`. */
|
|
54
|
+
unverifiedBlocking?: 'ignore' | 'adaptive' | 'recovery';
|
|
55
|
+
}
|
|
56
|
+
export interface ResultToConsume {
|
|
57
|
+
awaitId: string;
|
|
58
|
+
stepId: string | null;
|
|
59
|
+
purpose: string;
|
|
60
|
+
payload: Json | null;
|
|
61
|
+
late: boolean;
|
|
62
|
+
basisSeq: number;
|
|
63
|
+
resolvedAtSeq: number;
|
|
64
|
+
}
|
|
65
|
+
export interface Assessment {
|
|
66
|
+
taskId: string;
|
|
67
|
+
status: TaskState['status'];
|
|
68
|
+
mode: ResumeMode;
|
|
69
|
+
headSeq: number;
|
|
70
|
+
reasons: Reason[];
|
|
71
|
+
/** Steps that can run right now (deps closed, their awaits resolved). */
|
|
72
|
+
nextSteps: PlanStepState[];
|
|
73
|
+
planComplete: boolean;
|
|
74
|
+
/** Results that arrived but whose step has not been closed yet. */
|
|
75
|
+
results: ResultToConsume[];
|
|
76
|
+
pendingAwaits: Array<Pick<AwaitState, 'awaitId' | 'purpose' | 'stepId' | 'deadlineAt' | 'registeredAtSeq'>>;
|
|
77
|
+
assumptionChanges: AssumptionChange[];
|
|
78
|
+
unresolvedEffects: Array<{
|
|
79
|
+
key: string;
|
|
80
|
+
description: string;
|
|
81
|
+
intendedAtSeq: number;
|
|
82
|
+
inDoubt: boolean;
|
|
83
|
+
uncertainFailure: boolean;
|
|
84
|
+
}>;
|
|
85
|
+
/** Events by someone other than the sender, after the oldest unconsumed result's request was sent. */
|
|
86
|
+
sinceSend: Array<{
|
|
87
|
+
seq: number;
|
|
88
|
+
type: string;
|
|
89
|
+
at: string;
|
|
90
|
+
actor: string | null;
|
|
91
|
+
key: string | null;
|
|
92
|
+
}>;
|
|
93
|
+
/** False while another owner holds a live lease. */
|
|
94
|
+
claimable: boolean;
|
|
95
|
+
}
|
|
96
|
+
export declare function leaseIsLive(state: TaskState, now: Date): boolean;
|
|
97
|
+
export declare function assessTask(state: TaskState, events: readonly TaskEvent[], opts: AssessOptions): Assessment;
|
|
98
|
+
export interface StateChange {
|
|
99
|
+
section: 'plan' | 'assumptions' | 'observations' | 'effects' | 'awaits' | 'decisions' | 'status';
|
|
100
|
+
key: string;
|
|
101
|
+
change: 'added' | 'removed' | 'changed';
|
|
102
|
+
}
|
|
103
|
+
/** What differs between `before` (e.g. the state when a request was sent) and `after` (now). */
|
|
104
|
+
export declare function diffStates(before: TaskState, after: TaskState): StateChange[];
|
|
105
|
+
//# sourceMappingURL=assess.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assess.d.ts","sourceRoot":"","sources":["../../src/durable/assess.ts"],"names":[],"mappings":"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"}
|