canopycms 0.0.65 → 0.0.66-int.82
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/README.md +1 -3
- package/dist/ai/generate.d.ts +13 -0
- package/dist/ai/generate.js +9 -3
- package/dist/ai/to-plain-text.js +4 -0
- package/dist/ai/types.d.ts +20 -1
- package/dist/api/admin-branch-health.js +13 -12
- package/dist/api/branch.js +2 -2
- package/dist/branch-health.js +1 -1
- package/dist/branch-metadata-file.d.ts +58 -0
- package/dist/branch-metadata-file.js +65 -0
- package/dist/branch-metadata.d.ts +2 -26
- package/dist/branch-metadata.js +6 -33
- package/dist/branch-registry.js +4 -2
- package/dist/build/generate-ai-content.js +103 -0
- package/dist/cli/cli.js +432 -317
- package/dist/cli/generate-ai-content.js +264 -160
- package/dist/content-id-index.js +22 -0
- package/dist/content-listing.js +3 -0
- package/dist/content-reader.d.ts +25 -1
- package/dist/content-reader.js +15 -0
- package/dist/content-store.d.ts +50 -1
- package/dist/content-store.js +102 -4
- package/dist/context.d.ts +36 -4
- package/dist/context.js +15 -4
- package/dist/editor/admin/SystemHealthPanel.js +2 -2
- package/dist/entry-schema.d.ts +22 -0
- package/dist/git-manager.d.ts +9 -8
- package/dist/git-manager.js +35 -9
- package/dist/github-service.d.ts +1 -1
- package/dist/github-service.js +1 -1
- package/dist/paths/branch-name.d.ts +1 -1
- package/dist/paths/branch-name.js +1 -1
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema/index.js +1 -1
- package/dist/schema/meta-loader.js +1 -1
- package/dist/services.d.ts +22 -0
- package/dist/types.d.ts +1 -1
- package/dist/url-exclusivity-fixtures.d.ts +76 -0
- package/dist/url-exclusivity-fixtures.js +119 -0
- package/dist/url-path-resolver.js +8 -4
- package/dist/utils/content-write-lock.d.ts +2 -1
- package/dist/utils/content-write-lock.js +2 -1
- package/dist/utils/error.d.ts +17 -0
- package/dist/utils/error.js +17 -0
- package/dist/utils/git.d.ts +1 -1
- package/dist/utils/git.js +1 -1
- package/dist/utils/occ-json-write.js +1 -1
- package/dist/worker/cms-worker.d.ts +22 -288
- package/dist/worker/cms-worker.js +118 -1995
- package/dist/worker/git-sync.d.ts +166 -0
- package/dist/worker/git-sync.js +554 -0
- package/dist/worker/history-rewrite.d.ts +129 -0
- package/dist/worker/history-rewrite.js +216 -0
- package/dist/worker/rebase.d.ts +171 -0
- package/dist/worker/rebase.js +859 -0
- package/dist/worker/task-runner.d.ts +93 -0
- package/dist/worker/task-runner.js +529 -0
- package/dist/worker/worker-context.d.ts +133 -0
- package/dist/worker/worker-context.js +1 -0
- package/package.json +1 -1
|
@@ -27,7 +27,7 @@ export function sanitizeBranchName(branchName) {
|
|
|
27
27
|
* operating-mode/deployment-name.ts's `resolveDeploymentName`). Exported from
|
|
28
28
|
* this dependency-free module -- not constructed ad hoc at each call site --
|
|
29
29
|
* so every consumer that needs to recognize the settings-branch namespace
|
|
30
|
-
* agrees on the exact string: worker/
|
|
30
|
+
* agrees on the exact string: worker/git-sync.ts's `pushSettingsBranches`
|
|
31
31
|
* (never push a `canopycms-settings-*` branch this deployment doesn't own),
|
|
32
32
|
* and api/branch.ts's `createBranchHandler` (reject a user-requested branch
|
|
33
33
|
* whose SANITIZED name falls in this namespace).
|
package/dist/schema/index.d.ts
CHANGED
package/dist/schema/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { z } from 'zod';
|
|
|
4
4
|
import chokidar from 'chokidar';
|
|
5
5
|
import { getErrorMessage } from '../utils/error.js';
|
|
6
6
|
// canopyLogError/canopyLogWarn, not console.*: this module sits in the
|
|
7
|
-
// worker's runtime import closure (worker/
|
|
7
|
+
// worker's runtime import closure (worker/git-sync.ts and worker/rebase.ts ->
|
|
8
8
|
// content-index-generation.ts -> branch-schema-cache.ts -> schema/resolver.ts
|
|
9
9
|
// -> here), so a bare console line would land in worker.log without the
|
|
10
10
|
// ISO-8601 prefix the CloudWatch agent's multi_line_start_pattern needs (see
|
package/dist/services.d.ts
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `CanopyServices` — the long-lived, per-deployment service container.
|
|
3
|
+
*
|
|
4
|
+
* Built once by `createCanopyServices` and threaded through `ApiContext.services`
|
|
5
|
+
* to every API handler. Holds config, the schema registry and cache, the ACL
|
|
6
|
+
* checkers, the git-manager factory, the branch registry, the GitHub service, and
|
|
7
|
+
* four branch-workflow operations (`commitFiles`, `submitBranch`,
|
|
8
|
+
* `commitToSettingsBranch`, `getSettingsBranchRoot`).
|
|
9
|
+
*
|
|
10
|
+
* SCOPE WARNING: this is a service locator with 15 members, reached by every one of
|
|
11
|
+
* the 26 api modules, so adding a field is free and therefore constant — and no handler's
|
|
12
|
+
* signature reveals what it actually touches. Prefer giving a new capability its
|
|
13
|
+
* own narrow interface over a 23rd field here.
|
|
14
|
+
*
|
|
15
|
+
* Not to be confused with `context.ts`, which is the per-REQUEST facade built on
|
|
16
|
+
* top of this.
|
|
17
|
+
*
|
|
18
|
+
* `createTestCanopyServices` is test-only despite living in a production module;
|
|
19
|
+
* it reaches the published surface through a wildcard re-export.
|
|
20
|
+
*
|
|
21
|
+
* Module map: ./AGENTS.md.
|
|
22
|
+
*/
|
|
1
23
|
import type { CanopyConfig } from './config/index.js';
|
|
2
24
|
import type { EntrySchemaRegistry } from './schema/types.js';
|
|
3
25
|
import type { BranchContext } from './types.js';
|
package/dist/types.d.ts
CHANGED
|
@@ -140,7 +140,7 @@ export interface WorkerStatusReport {
|
|
|
140
140
|
}[];
|
|
141
141
|
/**
|
|
142
142
|
* Outcome of reconciling remote.git's `refs/heads/*` against GitHub's
|
|
143
|
-
* fetched tips (worker/
|
|
143
|
+
* fetched tips (worker/git-sync.ts's `reconcileTrackedBranches`) --
|
|
144
144
|
* the non-destructive replacement for the old fetch refspec that used
|
|
145
145
|
* to write GitHub's refs directly into `refs/heads/*`.
|
|
146
146
|
*/
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one-URL-per-entry invariant, as a reusable probe + report.
|
|
3
|
+
*
|
|
4
|
+
* `listEntries` assigns every entry exactly one `urlPath` and documents the round trip through
|
|
5
|
+
* `readByUrlPath` as safe. The reverse direction has repeatedly been the looser of the two: it
|
|
6
|
+
* has answered at URLs no forward surface emits, and those extra URLs were found one at a time,
|
|
7
|
+
* a release apart, by adopters (see
|
|
8
|
+
* `.claude/future-tasks/resolved/url-resolver-index-entry-extra-url.md`, then
|
|
9
|
+
* `resolved/readbyurlpath-entry-type-candidate-phantom-url.md`). This module exists so the whole
|
|
10
|
+
* invariant is asserted at once instead: enumerate, then probe every ADJACENT URL the resolver
|
|
11
|
+
* would try and require it to be a miss.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately free of `vitest` -- this is a plain `src` module in the shape of
|
|
14
|
+
* `operating-mode/deployment-name-fixtures.ts`, so more than one test file can import it without
|
|
15
|
+
* dragging that file's `vi.mock` calls and `describe` blocks along. It therefore ASSERTS NOTHING:
|
|
16
|
+
* it returns a report and the caller does the expecting, which also puts the offending URLs in
|
|
17
|
+
* the assertion message rather than behind a boolean.
|
|
18
|
+
*
|
|
19
|
+
* Scope of the invariant it checks: entries written in the `{type}.{slug}.{id}.{ext}` grammar
|
|
20
|
+
* with a type their collection declares -- i.e. exactly the set `listEntries` can see. A legacy
|
|
21
|
+
* untyped file (`overview.json`) is invisible to enumeration and is deliberately NOT probed here;
|
|
22
|
+
* see `.claude/future-tasks/legacy-untyped-files-url-addressable.md`.
|
|
23
|
+
*/
|
|
24
|
+
import { type RootCollectionConfig } from './config/index.js';
|
|
25
|
+
import type { ListEntriesItem } from './content-listing.js';
|
|
26
|
+
import type { CanopyContext } from './context.js';
|
|
27
|
+
import { type DuplicateUrlPath } from './static/index.js';
|
|
28
|
+
/**
|
|
29
|
+
* Every URL adjacent to the published set that the resolver would actually attempt.
|
|
30
|
+
*
|
|
31
|
+
* Three families, each one a shape that HAS resolved at some point in this package's history:
|
|
32
|
+
*
|
|
33
|
+
* 1. `/<collection>/<entryTypeName>` -- `resolveUrlPathCandidates`' index-fallback candidate lands
|
|
34
|
+
* on a registered entry-TYPE schema item, which `ContentStore.buildPaths` delegates to the
|
|
35
|
+
* parent collection with slug `index`, resolving that collection's index entry.
|
|
36
|
+
* 2. `/<collection>/<entryTypeName>/<slug>` -- the same delegation reached through the
|
|
37
|
+
* DIRECT-entry candidate instead, resolving the collection's entry `<slug>`. Wider than family
|
|
38
|
+
* 1: it needs no index entry at all, so it applies to every listed entry.
|
|
39
|
+
* 3. `/<publishedUrl>/index`, and its case variants -- an index entry answering at the literal
|
|
40
|
+
* spelling its collapsed URL replaced.
|
|
41
|
+
*
|
|
42
|
+
* Entry-type names are appended VERBATIM, after `computeEntryUrl` has lowercased the collection
|
|
43
|
+
* part. That asymmetry is real, not sloppiness: `flattenSchema` puts `entryType.name` into the
|
|
44
|
+
* logical path unchanged and `normalizeFilesystemPath` does not lowercase, so only the declared
|
|
45
|
+
* spelling can reach the entry-type schema item at all. A lowercased probe would miss the schema
|
|
46
|
+
* item, return null for the wrong reason, and pass vacuously.
|
|
47
|
+
*/
|
|
48
|
+
export declare const buildProbeUrls: (schema: RootCollectionConfig, items: readonly ListEntriesItem[], contentRoot?: string) => string[];
|
|
49
|
+
export interface UrlExclusivityReport {
|
|
50
|
+
/** Every `urlPath` the listing published, sorted. */
|
|
51
|
+
published: string[];
|
|
52
|
+
/** URLs claimed by more than one entry. A precondition failure, not a resolver finding. */
|
|
53
|
+
duplicates: DuplicateUrlPath[];
|
|
54
|
+
/** Published URLs `readByUrlPath` did NOT resolve -- the round trip broken going forward. */
|
|
55
|
+
unresolved: string[];
|
|
56
|
+
/** Published URLs that resolved to some OTHER entry than the one that published them. */
|
|
57
|
+
mismatched: Array<{
|
|
58
|
+
urlPath: string;
|
|
59
|
+
expectedEntryId?: string;
|
|
60
|
+
actualEntryId?: string;
|
|
61
|
+
}>;
|
|
62
|
+
/** Every adjacent URL probed, sorted. */
|
|
63
|
+
probes: string[];
|
|
64
|
+
/** Probes that resolved despite not being published -- the phantom URLs. Must be empty. */
|
|
65
|
+
phantoms: string[];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Enumerate, round-trip, then probe. See `UrlExclusivityReport` for what each field means and
|
|
69
|
+
* `buildProbeUrls` for which URLs are probed.
|
|
70
|
+
*
|
|
71
|
+
* A probe whose LOWERCASED form is published is skipped rather than asserted on: a collection
|
|
72
|
+
* literally named `index` publishes `/docs/index`, which family 3 also generates, and whether
|
|
73
|
+
* `/docs/Index` should resolve is a separate, pre-existing question about collection-path case
|
|
74
|
+
* sensitivity that this invariant does not speak to.
|
|
75
|
+
*/
|
|
76
|
+
export declare const collectUrlExclusivityReport: (ctx: Pick<CanopyContext, "listEntries" | "readByUrlPath">, schema: RootCollectionConfig, contentRoot?: string) => Promise<UrlExclusivityReport>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one-URL-per-entry invariant, as a reusable probe + report.
|
|
3
|
+
*
|
|
4
|
+
* `listEntries` assigns every entry exactly one `urlPath` and documents the round trip through
|
|
5
|
+
* `readByUrlPath` as safe. The reverse direction has repeatedly been the looser of the two: it
|
|
6
|
+
* has answered at URLs no forward surface emits, and those extra URLs were found one at a time,
|
|
7
|
+
* a release apart, by adopters (see
|
|
8
|
+
* `.claude/future-tasks/resolved/url-resolver-index-entry-extra-url.md`, then
|
|
9
|
+
* `resolved/readbyurlpath-entry-type-candidate-phantom-url.md`). This module exists so the whole
|
|
10
|
+
* invariant is asserted at once instead: enumerate, then probe every ADJACENT URL the resolver
|
|
11
|
+
* would try and require it to be a miss.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately free of `vitest` -- this is a plain `src` module in the shape of
|
|
14
|
+
* `operating-mode/deployment-name-fixtures.ts`, so more than one test file can import it without
|
|
15
|
+
* dragging that file's `vi.mock` calls and `describe` blocks along. It therefore ASSERTS NOTHING:
|
|
16
|
+
* it returns a report and the caller does the expecting, which also puts the offending URLs in
|
|
17
|
+
* the assertion message rather than behind a boolean.
|
|
18
|
+
*
|
|
19
|
+
* Scope of the invariant it checks: entries written in the `{type}.{slug}.{id}.{ext}` grammar
|
|
20
|
+
* with a type their collection declares -- i.e. exactly the set `listEntries` can see. A legacy
|
|
21
|
+
* untyped file (`overview.json`) is invisible to enumeration and is deliberately NOT probed here;
|
|
22
|
+
* see `.claude/future-tasks/legacy-untyped-files-url-addressable.md`.
|
|
23
|
+
*/
|
|
24
|
+
import { flattenSchema } from './config/index.js';
|
|
25
|
+
import { findDuplicateUrlPaths } from './static/index.js';
|
|
26
|
+
import { computeEntryUrl } from './utils/entry-url.js';
|
|
27
|
+
/** Append raw segments to a URL base, without normalizing their case (see buildProbeUrls). */
|
|
28
|
+
const joinUrl = (base, ...segments) => (base === '/' ? '' : base) + segments.map((s) => `/${s}`).join('');
|
|
29
|
+
/**
|
|
30
|
+
* Every URL adjacent to the published set that the resolver would actually attempt.
|
|
31
|
+
*
|
|
32
|
+
* Three families, each one a shape that HAS resolved at some point in this package's history:
|
|
33
|
+
*
|
|
34
|
+
* 1. `/<collection>/<entryTypeName>` -- `resolveUrlPathCandidates`' index-fallback candidate lands
|
|
35
|
+
* on a registered entry-TYPE schema item, which `ContentStore.buildPaths` delegates to the
|
|
36
|
+
* parent collection with slug `index`, resolving that collection's index entry.
|
|
37
|
+
* 2. `/<collection>/<entryTypeName>/<slug>` -- the same delegation reached through the
|
|
38
|
+
* DIRECT-entry candidate instead, resolving the collection's entry `<slug>`. Wider than family
|
|
39
|
+
* 1: it needs no index entry at all, so it applies to every listed entry.
|
|
40
|
+
* 3. `/<publishedUrl>/index`, and its case variants -- an index entry answering at the literal
|
|
41
|
+
* spelling its collapsed URL replaced.
|
|
42
|
+
*
|
|
43
|
+
* Entry-type names are appended VERBATIM, after `computeEntryUrl` has lowercased the collection
|
|
44
|
+
* part. That asymmetry is real, not sloppiness: `flattenSchema` puts `entryType.name` into the
|
|
45
|
+
* logical path unchanged and `normalizeFilesystemPath` does not lowercase, so only the declared
|
|
46
|
+
* spelling can reach the entry-type schema item at all. A lowercased probe would miss the schema
|
|
47
|
+
* item, return null for the wrong reason, and pass vacuously.
|
|
48
|
+
*/
|
|
49
|
+
export const buildProbeUrls = (schema, items, contentRoot = 'content') => {
|
|
50
|
+
const collections = flattenSchema(schema, contentRoot).filter((i) => i.type === 'collection');
|
|
51
|
+
const probes = new Set();
|
|
52
|
+
for (const collection of collections) {
|
|
53
|
+
const base = computeEntryUrl(collection.logicalPath, '', contentRoot);
|
|
54
|
+
for (const entryType of collection.entries ?? []) {
|
|
55
|
+
// Family 1.
|
|
56
|
+
probes.add(joinUrl(base, entryType.name));
|
|
57
|
+
// Family 2 -- every listed entry of this collection, under every type name the collection
|
|
58
|
+
// declares (not just the entry's OWN type: the directory scan `buildPaths` runs matches on
|
|
59
|
+
// slug alone, so any declared name reaches any entry).
|
|
60
|
+
for (const item of items) {
|
|
61
|
+
if (String(item.collectionPath) !== String(collection.logicalPath))
|
|
62
|
+
continue;
|
|
63
|
+
probes.add(joinUrl(base, entryType.name, item.slug));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Family 3.
|
|
68
|
+
for (const item of items) {
|
|
69
|
+
for (const spelling of ['index', 'Index', 'INDEX']) {
|
|
70
|
+
probes.add(joinUrl(item.urlPath, spelling));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return [...probes];
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Enumerate, round-trip, then probe. See `UrlExclusivityReport` for what each field means and
|
|
77
|
+
* `buildProbeUrls` for which URLs are probed.
|
|
78
|
+
*
|
|
79
|
+
* A probe whose LOWERCASED form is published is skipped rather than asserted on: a collection
|
|
80
|
+
* literally named `index` publishes `/docs/index`, which family 3 also generates, and whether
|
|
81
|
+
* `/docs/Index` should resolve is a separate, pre-existing question about collection-path case
|
|
82
|
+
* sensitivity that this invariant does not speak to.
|
|
83
|
+
*/
|
|
84
|
+
export const collectUrlExclusivityReport = async (ctx, schema, contentRoot = 'content') => {
|
|
85
|
+
const items = await ctx.listEntries();
|
|
86
|
+
const unresolved = [];
|
|
87
|
+
const mismatched = [];
|
|
88
|
+
for (const item of items) {
|
|
89
|
+
const result = await ctx.readByUrlPath(item.urlPath);
|
|
90
|
+
if (!result) {
|
|
91
|
+
unresolved.push(item.urlPath);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (result.meta.entryId !== item.entryId) {
|
|
95
|
+
mismatched.push({
|
|
96
|
+
urlPath: item.urlPath,
|
|
97
|
+
expectedEntryId: item.entryId,
|
|
98
|
+
actualEntryId: result.meta.entryId,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const publishedLower = new Set(items.map((i) => i.urlPath.toLowerCase()));
|
|
103
|
+
const probes = buildProbeUrls(schema, items, contentRoot);
|
|
104
|
+
const phantoms = [];
|
|
105
|
+
for (const probe of probes) {
|
|
106
|
+
if (publishedLower.has(probe.toLowerCase()))
|
|
107
|
+
continue;
|
|
108
|
+
if (await ctx.readByUrlPath(probe))
|
|
109
|
+
phantoms.push(probe);
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
published: items.map((i) => i.urlPath).sort(),
|
|
113
|
+
duplicates: findDuplicateUrlPaths(items),
|
|
114
|
+
unresolved: unresolved.sort(),
|
|
115
|
+
mismatched,
|
|
116
|
+
probes: [...probes].sort(),
|
|
117
|
+
phantoms: phantoms.sort(),
|
|
118
|
+
};
|
|
119
|
+
};
|
|
@@ -34,10 +34,14 @@ export function resolveUrlPathCandidates(urlPath, contentRoot) {
|
|
|
34
34
|
// `index` is not a contrived segment either — it is the slug the index convention requires on
|
|
35
35
|
// disk, so the collision was structural rather than accidental.
|
|
36
36
|
//
|
|
37
|
-
// This closes the `.../index` spelling
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
37
|
+
// This closes the `.../index` spelling only. The other extra URLs an entry answered at —
|
|
38
|
+
// `/<collection>/<entryTypeName>` via candidate 2, and `/<collection>/<entryTypeName>/<slug>`
|
|
39
|
+
// via candidate 1 — are closed DOWNSTREAM instead, by `readByUrlPath` requiring every
|
|
40
|
+
// candidate's `entryPath` to be a collection (`ReadContentInput.urlAddressableOnly`). They have
|
|
41
|
+
// to be: telling those apart needs the branch's schema, and this module is deliberately pure
|
|
42
|
+
// and schema-free so that the candidate shapes stay a fact about URLs rather than about
|
|
43
|
+
// content. The consequence to keep in mind when reading this file: the candidates below are
|
|
44
|
+
// what is ATTEMPTED, not what can resolve.
|
|
41
45
|
//
|
|
42
46
|
// Compared case-INSENSITIVELY, through the shared `isIndexSlug`. This function is the one
|
|
43
47
|
// consumer that sees a raw, un-normalized URL segment — everything downstream lowercases
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* Production runs a Lambda (API) and an EC2 worker over one EFS filesystem.
|
|
6
6
|
* `ContentStore`'s write/delete/rename critical sections were guarded only by
|
|
7
7
|
* the in-process mutex (`utils/async-mutex`), which does not cross that
|
|
8
|
-
* boundary, while
|
|
8
|
+
* boundary, while the worker's rebase loop (`rebaseOneBranch` in
|
|
9
|
+
* worker/rebase.ts) rewrites the very same
|
|
9
10
|
* working tree. A write landing after `git rebase` started is destroyed two
|
|
10
11
|
* ways -- `git checkout --theirs <file>` overwrites it with the branch's
|
|
11
12
|
* committed version (and the rebase then SUCCEEDS, logging nothing), and
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* Production runs a Lambda (API) and an EC2 worker over one EFS filesystem.
|
|
6
6
|
* `ContentStore`'s write/delete/rename critical sections were guarded only by
|
|
7
7
|
* the in-process mutex (`utils/async-mutex`), which does not cross that
|
|
8
|
-
* boundary, while
|
|
8
|
+
* boundary, while the worker's rebase loop (`rebaseOneBranch` in
|
|
9
|
+
* worker/rebase.ts) rewrites the very same
|
|
9
10
|
* working tree. A write landing after `git rebase` started is destroyed two
|
|
10
11
|
* ways -- `git checkout --theirs <file>` overwrites it with the branch's
|
|
11
12
|
* committed version (and the rebase then SUCCEEDS, logging nothing), and
|
package/dist/utils/error.d.ts
CHANGED
|
@@ -35,6 +35,23 @@ export declare function getErrorMessage(err: unknown): string;
|
|
|
35
35
|
* (CMS-internal layout like `.canopy-dev/remote.git` is useful for debugging
|
|
36
36
|
* and not sensitive); absolute paths outside it are replaced with `<path>`.
|
|
37
37
|
*/
|
|
38
|
+
/**
|
|
39
|
+
* TAG: `[REDACT]`
|
|
40
|
+
*
|
|
41
|
+
* Grep `[REDACT]` to find every site that persists or surfaces raw error text on
|
|
42
|
+
* a path that reaches a browser — worker task errors folded into
|
|
43
|
+
* `worker-status.json`, `branch.json` parse failures served by the admin
|
|
44
|
+
* branch-health endpoint, rebase-failure messages, and their tests.
|
|
45
|
+
*
|
|
46
|
+
* Those sites carried the tag `[HIGH-1]`/`[MEDIUM-2]` until 2026-08-23, IDs from
|
|
47
|
+
* a review pass whose findings list was never committed — 13 occurrences across
|
|
48
|
+
* 6 files resolving to nothing. Renamed rather than deleted because the
|
|
49
|
+
* underlying rule is real and genuinely cross-file: a Node error embeds absolute
|
|
50
|
+
* paths, and a git remote URL can embed a token, so anything on that path is
|
|
51
|
+
* redacted through the function below.
|
|
52
|
+
*
|
|
53
|
+
* The tag is a grep aid, not the rule. The rule is stated at each site.
|
|
54
|
+
*/
|
|
38
55
|
export declare function sanitizeErrorMessage(message: string): string;
|
|
39
56
|
/**
|
|
40
57
|
* Redact only credential material from a message, leaving filesystem paths
|
package/dist/utils/error.js
CHANGED
|
@@ -43,6 +43,23 @@ export function getErrorMessage(err) {
|
|
|
43
43
|
* (CMS-internal layout like `.canopy-dev/remote.git` is useful for debugging
|
|
44
44
|
* and not sensitive); absolute paths outside it are replaced with `<path>`.
|
|
45
45
|
*/
|
|
46
|
+
/**
|
|
47
|
+
* TAG: `[REDACT]`
|
|
48
|
+
*
|
|
49
|
+
* Grep `[REDACT]` to find every site that persists or surfaces raw error text on
|
|
50
|
+
* a path that reaches a browser — worker task errors folded into
|
|
51
|
+
* `worker-status.json`, `branch.json` parse failures served by the admin
|
|
52
|
+
* branch-health endpoint, rebase-failure messages, and their tests.
|
|
53
|
+
*
|
|
54
|
+
* Those sites carried the tag `[HIGH-1]`/`[MEDIUM-2]` until 2026-08-23, IDs from
|
|
55
|
+
* a review pass whose findings list was never committed — 13 occurrences across
|
|
56
|
+
* 6 files resolving to nothing. Renamed rather than deleted because the
|
|
57
|
+
* underlying rule is real and genuinely cross-file: a Node error embeds absolute
|
|
58
|
+
* paths, and a git remote URL can embed a token, so anything on that path is
|
|
59
|
+
* redacted through the function below.
|
|
60
|
+
*
|
|
61
|
+
* The tag is a grep aid, not the rule. The rule is stated at each site.
|
|
62
|
+
*/
|
|
46
63
|
export function sanitizeErrorMessage(message) {
|
|
47
64
|
let result = redactCredentials(message);
|
|
48
65
|
// Paths under the project root become relative (split/join avoids regex
|
package/dist/utils/git.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export declare function isNetworkRemoteUrl(url: string): boolean;
|
|
|
27
27
|
*
|
|
28
28
|
* Deliberately narrow: ordinary transient push failures -- network drops,
|
|
29
29
|
* auth/permission denial, lock contention -- must keep retrying with backoff
|
|
30
|
-
* (see worker/
|
|
30
|
+
* (see worker/task-runner.ts's `isPermanentTaskFailure`, which treats git
|
|
31
31
|
* failures as transient precisely so those cases still get retried). Only
|
|
32
32
|
* this specific, structurally-unretryable shape should fail fast (worker) or
|
|
33
33
|
* return 409 (API) instead.
|
package/dist/utils/git.js
CHANGED
|
@@ -79,7 +79,7 @@ const REJECTION_HINT = 'Updates were rejected because';
|
|
|
79
79
|
*
|
|
80
80
|
* Deliberately narrow: ordinary transient push failures -- network drops,
|
|
81
81
|
* auth/permission denial, lock contention -- must keep retrying with backoff
|
|
82
|
-
* (see worker/
|
|
82
|
+
* (see worker/task-runner.ts's `isPermanentTaskFailure`, which treats git
|
|
83
83
|
* failures as transient precisely so those cases still get retried). Only
|
|
84
84
|
* this specific, structurally-unretryable shape should fail fast (worker) or
|
|
85
85
|
* return 409 (API) instead.
|
|
@@ -227,7 +227,7 @@ export async function withOccFileLock(filePath, fn) {
|
|
|
227
227
|
// process's lockfile heartbeat stops (crash) or the lock naturally
|
|
228
228
|
// expires, so this is logged rather than rethrown.
|
|
229
229
|
//
|
|
230
|
-
//
|
|
230
|
+
// ENOENT specifically is expected, not an error: the lock
|
|
231
231
|
// marker's target directory legitimately vanishes when an admin purge
|
|
232
232
|
// (see api/admin-branch-health.ts) renames the branch tree out from
|
|
233
233
|
// under a held lock. Every other release failure still warns.
|